AIdentity

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }