Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useState, useEffect } from 'react'; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogFooter, | |
| } from '@/components/ui/dialog'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Label } from '@/components/ui/label'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import { CheckboxField } from '@/components/ui/checkbox-field'; | |
| import { Loader2 } from 'lucide-react'; | |
| import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; | |
| interface ArticleGenerationModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onGenerate: (params: ArticleGenerationParams) => void; | |
| isGenerating: boolean; | |
| } | |
| export interface ArticleGenerationParams { | |
| publisher: string[]; | |
| grade: string[]; | |
| unit: string[]; | |
| textbookVocab: string; | |
| additionalVocab: string; | |
| grammar: string[]; | |
| topic: string[]; | |
| inputArticle: string; | |
| cefr: string; // single-select: A1..C2 | |
| } | |
| // Options based on the Python entry_form.py | |
| const PUBLISHER_OPTIONS = [ | |
| '康軒', '南一', '翰林' | |
| ]; | |
| const GRADE_OPTIONS = [ | |
| '七年級上學期', '七年級下學期', | |
| '八年級上學期', '八年級下學期', | |
| '九年級上學期', '九年級下學期' | |
| ]; | |
| const UNIT_OPTIONS = [ | |
| '第一課', '第二課', '第三課', '第四課', '第五課', '第六課' | |
| ]; | |
| const GRAMMAR_OPTIONS = [ | |
| '名詞', '動詞', '形容詞', '副詞', '介系詞', '連接詞', '助詞' | |
| ]; | |
| const TOPIC_OPTIONS = [ | |
| '家庭', '學校', '運動', '食物', '動物', '旅遊', '科技', '環境', '友誼', '夢想' | |
| ]; | |
| export function ArticleGenerationModal({ | |
| isOpen, | |
| onClose, | |
| onGenerate, | |
| isGenerating | |
| }: ArticleGenerationModalProps) { | |
| const [formData, setFormData] = useState<ArticleGenerationParams>({ | |
| publisher: [], | |
| grade: [], | |
| unit: [], | |
| textbookVocab: '', | |
| additionalVocab: '', | |
| grammar: [], | |
| topic: [], | |
| inputArticle: '', | |
| cefr: '' | |
| }); | |
| // Reset form when modal opens | |
| useEffect(() => { | |
| if (isOpen) { | |
| setFormData({ | |
| publisher: [], | |
| grade: [], | |
| unit: [], | |
| textbookVocab: '', | |
| additionalVocab: '', | |
| grammar: [], | |
| topic: [], | |
| inputArticle: '', | |
| cefr: '' | |
| }); | |
| } | |
| }, [isOpen]); | |
| // Update textbook vocabulary when selections change | |
| useEffect(() => { | |
| let cancelled = false; | |
| const fetchVocab = async () => { | |
| if (formData.grade.length > 0 && formData.unit.length > 0 && formData.publisher.length > 0) { | |
| try { | |
| const res = await fetch('/api/vocab', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| publisher: formData.publisher, | |
| grade: formData.grade, | |
| unit: formData.unit, | |
| }), | |
| }); | |
| if (!res.ok) throw new Error('Failed to fetch vocab'); | |
| const data: { vocab: string[]; text: string } = await res.json(); | |
| if (!cancelled) { | |
| setFormData(prev => ({ ...prev, textbookVocab: data.text })); | |
| } | |
| } catch (e) { | |
| if (!cancelled) { | |
| setFormData(prev => ({ ...prev, textbookVocab: '' })); | |
| } | |
| } | |
| } else { | |
| if (!cancelled) { | |
| setFormData(prev => ({ ...prev, textbookVocab: '' })); | |
| } | |
| } | |
| }; | |
| fetchVocab(); | |
| return () => { cancelled = true; }; | |
| }, [formData.grade, formData.unit, formData.publisher]); | |
| const handleCheckboxChange = (field: keyof ArticleGenerationParams, value: string) => { | |
| setFormData(prev => { | |
| const currentArray = prev[field] as string[]; | |
| const newArray = currentArray.includes(value) | |
| ? currentArray.filter(item => item !== value) | |
| : [...currentArray, value]; | |
| return { ...prev, [field]: newArray }; | |
| }); | |
| }; | |
| const handleTextChange = (field: keyof ArticleGenerationParams, value: string) => { | |
| setFormData(prev => ({ ...prev, [field]: value })); | |
| }; | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| onGenerate(formData); | |
| }; | |
| return ( | |
| <Dialog open={isOpen} onOpenChange={onClose}> | |
| <DialogContent className="max-w-4xl max-h-[90vh]"> | |
| <DialogHeader> | |
| <DialogTitle>📝 Generate / Rewrite Article</DialogTitle> | |
| <DialogDescription> | |
| Configure the parameters for article generation | |
| </DialogDescription> | |
| </DialogHeader> | |
| <ScrollArea className="h-[calc(90vh-200px)] pr-4"> | |
| <form onSubmit={handleSubmit} className="space-y-6"> | |
| {/* 初始文章 */} | |
| <div> | |
| <Label htmlFor="input-article" className="mb-2"> | |
| 文章 ( 如需改寫文章可輸入在此,無則留空 ) | |
| </Label> | |
| <Textarea | |
| id="input-article" | |
| value={formData.inputArticle} | |
| onChange={(e) => handleTextChange('inputArticle', e.target.value)} | |
| placeholder="Enter an initial article or topic to base the generation on..." | |
| className="min-h-[120px]" | |
| /> | |
| </div> | |
| {/* CEFR level */} | |
| <div className="grid grid-cols-1 gap-2"> | |
| <Label htmlFor="cefr" className="mb-2">CEFR Level ( 只會影響到文章,不會影響到題目 )</Label> | |
| <Select | |
| value={formData.cefr} | |
| onValueChange={(v) => setFormData(prev => ({ ...prev, cefr: v }))} | |
| > | |
| <SelectTrigger id="cefr" className="w-full"> | |
| <SelectValue placeholder="Select CEFR level (Below A1 - C2)" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="Below A1">Below A1</SelectItem> | |
| <SelectItem value="A1">A1</SelectItem> | |
| <SelectItem value="A2">A2</SelectItem> | |
| <SelectItem value="B1">B1</SelectItem> | |
| <SelectItem value="B2">B2</SelectItem> | |
| <SelectItem value="C1">C1</SelectItem> | |
| <SelectItem value="C2">C2</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| {/* 出版社 */} | |
| <CheckboxField | |
| label="出版社 (Publisher)" | |
| fieldName="publisher" | |
| value={formData.publisher} | |
| onChange={(value: string) => handleCheckboxChange('publisher', value)} | |
| options={PUBLISHER_OPTIONS} | |
| columns={1} | |
| /> | |
| {/* 學生年級 + 課程範圍 */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <CheckboxField | |
| label="學生年級 (Grade Level)" | |
| fieldName="grade" | |
| value={formData.grade} | |
| onChange={(value: string) => handleCheckboxChange('grade', value)} | |
| options={GRADE_OPTIONS} | |
| columns={1} | |
| /> | |
| <CheckboxField | |
| label="課程範圍 (Unit Range)" | |
| fieldName="unit" | |
| value={formData.unit} | |
| onChange={(value: string) => handleCheckboxChange('unit', value)} | |
| options={UNIT_OPTIONS} | |
| columns={1} | |
| /> | |
| </div> | |
| {/* 文法範圍 + 主題範圍 */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <CheckboxField | |
| label="文法範圍 (Grammar Focus)" | |
| fieldName="grammar" | |
| value={formData.grammar} | |
| onChange={(value: string) => handleCheckboxChange('grammar', value)} | |
| options={GRAMMAR_OPTIONS} | |
| columns={1} | |
| /> | |
| <CheckboxField | |
| label="主題範圍 (Topic)" | |
| fieldName="topic" | |
| value={formData.topic} | |
| onChange={(value: string) => handleCheckboxChange('topic', value)} | |
| options={TOPIC_OPTIONS} | |
| columns={1} | |
| /> | |
| </div> | |
| {/* 單字列表 */} | |
| <div> | |
| <Label htmlFor="textbook-vocab" className="mb-2"> | |
| 課本單字列表 (Textbook Vocabulary) | |
| </Label> | |
| <Textarea | |
| id="textbook-vocab" | |
| value={formData.textbookVocab} | |
| readOnly | |
| placeholder="Select units to see vocabulary" | |
| className="bg-muted" | |
| /> | |
| </div> | |
| {/* 額外單字列表 */} | |
| <div> | |
| <Label htmlFor="additional-vocab" className="mb-2"> | |
| 額外單字列表 (Additional Vocabulary) | |
| </Label> | |
| <Textarea | |
| id="additional-vocab" | |
| value={formData.additionalVocab} | |
| onChange={(e) => handleTextChange('additionalVocab', e.target.value)} | |
| placeholder="Enter additional vocabulary words, separated by commas" | |
| /> | |
| </div> | |
| </form> | |
| </ScrollArea> | |
| <DialogFooter> | |
| <Button type="button" variant="outline" onClick={onClose}> | |
| Cancel | |
| </Button> | |
| <Button | |
| type="submit" | |
| onClick={handleSubmit} | |
| disabled={isGenerating} | |
| > | |
| {isGenerating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | |
| {isGenerating ? '生成中...' : '生成 / 改寫文章'} | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } |