6_chat.tsx (5026B)
1 import { KeyboardEvent, useEffect, useRef, useState } from "react"; 2 import { StageProps } from "../ctrl/page.tsx"; 3 export type QuestionWithAudio = { 4 input: string; // 質問文 5 ans: string; // 正しい回答 6 audioUrl: string; // 質問に関連付けられた音源のURL 7 }; 8 interface QuestionAnswerProps { 9 questions: QuestionWithAudio[]; 10 onComplete: () => void; // 全質問完了時のコールバック 11 } 12 const QuestionAnswer: React.FC<QuestionAnswerProps> = ({ 13 questions, 14 onComplete, 15 }) => { 16 const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); 17 const [currentAnswer, setCurrentAnswer] = useState(""); 18 const [errorMessage, setErrorMessage] = useState(""); 19 const audioRef = useRef<HTMLAudioElement | null>(null); 20 const latestStateRef = useRef({ 21 answerLength: 0, 22 questionIndex: 0, 23 isAnswerComplete: false, 24 }); 25 const currentQuestion = questions[currentQuestionIndex]; 26 const { input: questionText, ans: correctAnswer, audioUrl } = currentQuestion; 27 const nextCorrectKey = correctAnswer[currentAnswer.length]; 28 useEffect(() => { 29 latestStateRef.current = { 30 answerLength: currentAnswer.length, 31 questionIndex: currentQuestionIndex, 32 isAnswerComplete: currentAnswer.length === correctAnswer.length, 33 }; 34 }, [currentAnswer.length, currentQuestionIndex]); 35 useEffect(() => { 36 if (audioRef.current) { 37 audioRef.current.pause(); 38 audioRef.current.currentTime = 0; 39 audioRef.current.onended = null; 40 } 41 const audio = audioRef.current || new Audio(); 42 audioRef.current = audio; 43 audio.src = audioUrl; 44 audio.load(); 45 audio.onended = (event: Event) => { 46 const currentQuestionIdx = latestStateRef.current.questionIndex; 47 const nextIndex = currentQuestionIdx + 1; 48 if (nextIndex < questions.length) { 49 setCurrentQuestionIndex(nextIndex); 50 setCurrentAnswer(""); 51 setErrorMessage(""); 52 } else { 53 onComplete(); 54 } 55 }; 56 audio 57 .play() 58 .catch((e) => console.error("Audio playback failed on initial play:", e)); 59 return () => { 60 if (audioRef.current) { 61 audioRef.current.onended = null; 62 audioRef.current.pause(); 63 } 64 }; 65 }, [currentQuestionIndex, questions.length, audioUrl]); // 依存配列は音源切り替えに必要なもののみ 66 if (!currentQuestion) { 67 return <div className="p-4">🎉 すべての質問が完了しました!</div>; 68 } 69 const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { 70 if (e.key === "Enter") { 71 e.preventDefault(); 72 return; 73 } 74 if (currentAnswer.length >= correctAnswer.length) { 75 e.preventDefault(); 76 return; 77 } 78 if (e.key === nextCorrectKey) { 79 e.preventDefault(); // デフォルトの入力をキャンセル 80 setErrorMessage(""); // エラーをクリア 81 const newAnswer = currentAnswer + e.key; 82 setCurrentAnswer(newAnswer); 83 if (newAnswer.length === correctAnswer.length) { 84 } 85 } else { 86 e.preventDefault(); 87 setErrorMessage("🚫 その回答は正しくありません"); 88 } 89 }; 90 return ( 91 <article style={{}}> 92 <p style={{ textAlign: "center", fontSize: "5rem" }}>{questionText}</p> 93 <div style={{ textAlign: "center" }}> 94 <input 95 type="text" 96 value={currentAnswer} 97 onKeyDown={handleKeyDown} 98 readOnly 99 placeholder={correctAnswer 100 .split("") 101 .map(() => "_") 102 .join(" ")} 103 autoFocus 104 style={{ fontSize: "2rem" }} 105 /> 106 {currentAnswer.length === correctAnswer.length && <span></span>} 107 </div> 108 {errorMessage && <p>{errorMessage}</p>} 109 <p style={{ textAlign: "center", padding: "5vh" }}> 110 (日本語の回答は**ヘボン式ローマ字**のキー入力のみを想定しています) 111 </p> 112 </article> 113 ); 114 }; 115 const myQuestions: QuestionWithAudio[] = [ 116 { input: "5 + 9 = ?", ans: "14", audioUrl: "/audio/006-1.flac" }, 117 { 118 input: "人を殺してはいけませんか?", 119 ans: "yes", 120 audioUrl: "/audio/006-2.flac", 121 }, 122 { 123 input: "あなたは間違っていましたか?", 124 ans: "yes", 125 audioUrl: "/audio/006-3.flac", 126 }, 127 { 128 input: "あのときのこの人の気持ちを答えよ", 129 ans: "happy", 130 audioUrl: "/audio/006-4.flac", 131 }, 132 { 133 input: "あなたは間違っていましたか?", 134 ans: "yes", 135 audioUrl: "/audio/006-5.flac", 136 }, 137 { 138 input: "Does justice always prevail?", 139 ans: "yes", 140 audioUrl: "/audio/006-6.flac", 141 }, 142 ]; 143 export default function QuizPage({ onComplete }: StageProps) { 144 const handleQuizComplete = () => { 145 onComplete(); 146 }; 147 return ( 148 <div 149 style={{ 150 width: "100vw", 151 height: "100vh", 152 display: "flex", 153 justifyContent: "center", 154 alignItems: "center", 155 }} 156 > 157 <QuestionAnswer questions={myQuestions} onComplete={handleQuizComplete} /> 158 </div> 159 ); 160 }