第四話：AIエージェントの作り方

本日DAIMARUに行ったら、Diorでいつになく列ができていました。クリスマスでしたね。みなさん彼女にプレゼント奮発するんですね。非常に羨ましいですが、人は人、自分は自分で毎日着実に頑張っていきたいと思います。

本記事は下記の続編となります。

第一話：https://note.com/bletainasus/n/nab5840b01b4b

第二話：https://www.bletainasus.com/dashboard/post/second-story/

第三話：https://zenn.dev/takayamasashi/articles/9da050c90cbd0b

これまで社内Wiki用のAIアプリを開発し、販売するまでの工程を紹介してきました。

今回は初心者向けに、学習用にPDFを取り込んだ後に、編集と削除する機能の追加方法をご紹介いたします。

最初のUIはこんな感じでした。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_17.17.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/06462599-ad69-c14c-86fb-166df9e6c3bf.png)

英語を日本語にするのと、エージェントのCSSがグレーと白でわかりづらいので、調整します。

```jsx
// app/(preview)/page.tsx

"use client";

import { useState } from "react";
import { experimental_useObject } from "ai/react";
import { questionsSchema } from "@/lib/schemas";
import { z } from "zod";
import { toast } from "sonner";
import { FileUp, Plus, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import Quiz from "@/components/quiz";
import { Link } from "@/components/ui/link";
import { AnimatePresence, motion } from "framer-motion";
import ChatBot from "@/components/chatbot";

export default function ChatWithFiles() {
  const [files, setFiles] = useState<File[]>([]);
  const [questions, setQuestions] = useState<z.infer<typeof questionsSchema>>(
    []
  );
  const [isDragging, setIsDragging] = useState(false);
  const [title, setTitle] = useState<string>();

  const {
    submit,
    object: partialQuestions,
    isLoading,
  } = experimental_useObject({
    api: "/api/generate-quiz",
    schema: questionsSchema,
    initialValue: undefined,
    onError: (error) => {
      toast.error("Failed to generate quiz. Please try again.");
      setFiles([]);
    },
    onFinish: ({ object }) => {
      setQuestions(object ?? []);
    },
  });

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    if (isSafari && isDragging) {
      toast.error(
        "Safari does not support drag & drop. Please use the file picker."
      );
      return;
    }

    const selectedFiles = Array.from(e.target.files || []);
    const validFiles = selectedFiles.filter(
      (file) => file.type === "application/pdf" && file.size <= 5 * 1024 * 1024
    );
    console.log(validFiles);

    if (validFiles.length !== selectedFiles.length) {
      toast.error("Only PDF files under 5MB are allowed.");
    }

    setFiles(validFiles);
  };

  const encodeFileAsBase64 = (file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = (error) => reject(error);
    });
  };

  const handleSubmitWithFiles = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const encodedFiles = await Promise.all(
      files.map(async (file) => ({
        name: file.name,
        type: file.type,
        data: await encodeFileAsBase64(file),
      }))
    );
    submit({ files: encodedFiles });

    // ファイル名をタイトルとして設定
    setTitle(files[0].name); // 最初のファイル名をタイトルに設定
  };

  const clearPDF = () => {
    setFiles([]);
    setQuestions([]);
  };

  const progress = partialQuestions ? (partialQuestions.length / 4) * 100 : 0;

  if (questions.length === 4) {
    return (
      <Quiz
        pdf_name={title ?? "Quiz"}
        questions={questions}
        clearPDF={clearPDF}
      />
    );
  }

  return (
    <div
      className="min-h-[100dvh] w-full flex justify-center"
      onDragOver={(e) => {
        e.preventDefault();
        setIsDragging(true);
      }}
      onDragExit={() => setIsDragging(false)}
      onDragEnd={() => setIsDragging(false)}
      onDragLeave={() => setIsDragging(false)}
      onDrop={(e) => {
        e.preventDefault();
        setIsDragging(false);
        console.log(e.dataTransfer.files);
        handleFileChange({
          target: { files: e.dataTransfer.files },
        } as React.ChangeEvent<HTMLInputElement>);
      }}
    >
      <AnimatePresence>
        {isDragging && (
          <motion.div
            className="fixed pointer-events-none dark:bg-zinc-900/90 h-dvh w-dvw z-10 justify-center items-center flex flex-col gap-1 bg-zinc-100/90"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            <div>Drag and drop files here</div>
            <div className="text-sm dark:text-zinc-400 text-zinc-500">
              {"(PDFs only)"}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
      <Card className="w-full max-w-md h-full border-0 sm:border sm:h-fit mt-12">
        <CardHeader className="text-center space-y-6">
          <div className="mx-auto flex items-center justify-center space-x-2 text-muted-foreground">
            <div className="rounded-full bg-primary/10 p-2">
              <FileUp className="h-6 w-6" />
            </div>
            <Plus className="h-4 w-4" />
            <div className="rounded-full bg-primary/10 p-2">
              <Loader2 className="h-6 w-6" />
            </div>
          </div>
          <div className="space-y-2">
            <CardTitle className="text-2xl font-bold">Wikiだるま</CardTitle>
            <CardDescription className="text-base">
              PDFをアップロードすると、その内容に基づいてインタラクティブなQ&Aを生成します。その情報を元に雪だるま式に社内Wikiエージェントを生成するサービスです。
            </CardDescription>
          </div>
        </CardHeader>
        <CardContent>
          <form onSubmit={handleSubmitWithFiles} className="space-y-4">
            <div
              className={`relative flex flex-col items-center justify-center border-2 border-dashed border-muted-foreground/25 rounded-lg p-6 transition-colors hover:border-muted-foreground/50`}
            >
              <input
                type="file"
                onChange={handleFileChange}
                accept="application/pdf"
                className="absolute inset-0 opacity-0 cursor-pointer"
              />
              <FileUp className="h-8 w-8 mb-2 text-muted-foreground" />
              <p className="text-sm text-muted-foreground text-center">
                {files.length > 0 ? (
                  <span className="font-medium text-foreground">
                    {files[0].name}
                  </span>
                ) : (
                  <span>
                    ここにPDFをドラッグ＆ドロップするか、クリックしてファイルを選択してください。
                  </span>
                )}
              </p>
            </div>
            <Button
              type="submit"
              className="w-full"
              disabled={files.length === 0}
            >
              {isLoading ? (
                <span className="flex items-center space-x-2">
                  <Loader2 className="h-4 w-4 animate-spin" />
                  <span>Q&A生成中…</span>
                </span>
              ) : (
                "Q&A生成"
              )}
            </Button>
          </form>
        </CardContent>
        {isLoading && (
          <CardFooter className="flex flex-col space-y-4">
            <div className="w-full space-y-1">
              <div className="flex justify-between text-sm text-muted-foreground">
                <span>進捗</span>
                <span>{Math.round(progress)}%</span>
              </div>
              <Progress value={progress} className="h-2" />
            </div>
            <div className="w-full space-y-2">
              <div className="grid grid-cols-6 sm:grid-cols-4 items-center space-x-2 text-sm">
                <div
                  className={`h-2 w-2 rounded-full ${
                    isLoading ? "bg-yellow-500/50 animate-pulse" : "bg-muted"
                  }`}
                />
                <span className="text-muted-foreground text-center col-span-4 sm:col-span-2">
                  {partialQuestions
                    ? `データ抽出中 ${partialQuestions.length + 1} of 4`
                    : "PDF解析中"}
                </span>
              </div>
            </div>
          </CardFooter>
        )}
      </Card>
      <ChatBot />
    </div>
  );
}
```

CSS設定変更用。

```jsx
// components/chatbot.tsx

import { useState } from "react";
import { MessageCircle, X } from "lucide-react"; // Importing the chat icon and close icon

const ChatBot = () => {
  const [messages, setMessages] = useState<
    { content: string; type: "sent" | "received"; score?: number }[]
  >([]);
  const [isOpen, setIsOpen] = useState(false); // State to toggle the chatbot visibility
  const [inputValue, setInputValue] = useState(""); // State to manage input value

  const handleSendMessage = async (message: string) => {
    setMessages((prevMessages) => [
      ...prevMessages,
      { content: message, type: "sent" },
    ]);

    try {
      const response = await fetch("/api/chatbot", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ inputValue: message }),
      });

      const data = await response.json();

      //   console.log(data);

      if (data.error) {
        setMessages((prevMessages) => [
          ...prevMessages,
          {
            content: "申し訳ありませんが、処理中にエラーが発生しました。",
            type: "received",
          },
        ]);
      } else {
        setMessages((prevMessages) => [
          ...prevMessages,
          {
            content:
              data.answer ||
              data.closestQuestion ||
              "回答を取得できませんでした。",
            type: "received",
            score: data.score,
          },
        ]);
      }
    } catch (error) {
      console.error("Error in API request:", error);
      setMessages((prevMessages) => [
        ...prevMessages,
        {
          content: "申し訳ありませんが、エラーが発生しました。",
          type: "received",
        },
      ]);
    }
  };

  return (
    <>
      {/* Floating Chat Button */}
      <div
        onClick={() => setIsOpen(!isOpen)}
        className="fixed bottom-4 right-4 bg-primary text-background p-4 rounded-full cursor-pointer shadow-lg hover:bg-primary/80 transition"
      >
        <MessageCircle className="h-8 w-8 text-background" />
      </div>

      {/* ChatBot Modal */}
      {isOpen && (
        <div className="fixed bottom-16 right-4 w-80 max-w-full bg-white border rounded-lg shadow-lg p-4">
          {/* Close Button */}
          <div
            onClick={() => setIsOpen(false)} // Close the chatbot on button click
            className="absolute top-2 right-2 p-2 cursor-pointer text-muted-foreground hover:text-primary"
          >
            <X className="h-6 w-6" />
          </div>

          <div className="chat-header flex items-center space-x-2">
            <MessageCircle className="h-6 w-6 text-primary" />
            <span className="font-semibold text-lg">Chat with AI</span>
          </div>
          <div className="chat-messages space-y-2 mt-4 overflow-auto max-h-60">
            {messages.map((msg, index) => (
              <div
                key={index}
                className={`chat-message p-2 rounded-lg ${
                  msg.type === "sent"
                    ? "bg-gray-200 text-black"
                    : "bg-gray-200 text-black"
                }`}
              >
                <MessageCircle className="h-5 w-5 inline mr-2 text-muted-foreground" />
                <span>{msg.content}</span>
                {msg.score !== undefined && (
                  <div className="text-sm text-gray-500 mt-1">
                    <strong>類似性スコア:</strong> {msg.score.toFixed(2)}
                  </div>
                )}
              </div>
            ))}
          </div>

          <div className="chat-input mt-4 flex space-x-2">
            <textarea
              className="flex-1 p-2 border rounded-lg"
              placeholder="お手伝いできることはありますか？"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
            />
            <button
              className="bg-primary text-primary-foreground p-2 rounded-lg hover:bg-primary/80 hover:text-primary-foreground transition"
              onClick={() => {
                if (inputValue.trim()) {
                  handleSendMessage(inputValue);
                  setInputValue("");
                }
              }}
            >
              送信
            </button>
          </div>
        </div>
      )}
    </>
  );
};

export default ChatBot;

```

結果はこんな感じになります。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_17.38.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/1fcc057d-4705-c8cd-2177-81d0a82aa8db.png)

PDFを貼付し、Q&A生成を行います。

ここで失敗する方は、第一話をご参照いただけますと上手くいくと思います。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_17.39.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/08ffe7e9-e5d9-e841-a846-51b58ab1f830.png)

内容を読み取って、Q&Aを４つ生成してくれました。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_17.41.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/c6dafe9f-8c9a-cae5-b048-17295ba5dcfd.png)

この画面にて、生成されたQ&Aを編集と削除する機能を開発したいと思います。

まずはAIが間違いを起こしてしまった時用に、削除機能からいきます。

## 削除機能をつくる

まずスキーマを修正します。

```jsx
// lib/schemas.ts

import { z } from "zod";

// Update question schema to include an id and a single option
export const questionSchema = z.object({
  id: z.string(),  // or z.number() if you prefer numeric ids
  question: z.string(),
  option: z.string().describe("The single option for the question."), // Single option for the question
  answer: z.string().describe("The user's selected answer to the question."),
}).refine(
  (data) => data.option === data.answer, // Check if the answer is equal to the option
  { message: "Answer must be the provided option." }
);

export type Question = z.infer<typeof questionSchema>;

export const questionsSchema = z.array(questionSchema).length(4); // Assuming you still expect exactly 4 questions
```

次に、Q&Aを表示しているコンポーネントを修正します。

```jsx
// components/quiz.tsx

import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Check, X, RefreshCw, FileText } from "lucide-react";
import QuizReview from "./quiz-overview";
import { Question } from "@/lib/schemas";
import useSWR, { mutate } from "swr";

type QuizProps = {
  questions: Question[];
  clearPDF: () => void;
  pdf_name: string; // title を pdf_name に変更
};

const QuestionCard: React.FC<{
  question: Question;
  selectedAnswer: string | null;
  onSelectAnswer: (answer: string) => void;
  onDeleteAnswer: () => void; // 削除ハンドラー
  isSubmitted: boolean;
  showCorrectAnswer: boolean;
}> = ({
  question,
  selectedAnswer,
  onSelectAnswer,
  onDeleteAnswer,
  isSubmitted,
  showCorrectAnswer,
}) => {
  const options = Array.isArray(question.option)
    ? question.option
    : question.option
    ? [question.option]
    : []; // オプションを配列として扱う

  return (
    <div className="space-y-6">
      <h2 className="text-lg font-semibold leading-tight">
        {question.question}
      </h2>
      <div className="grid grid-cols-1 gap-4">
        {options.length > 0 && (
          <Button
            variant={selectedAnswer === "A" ? "secondary" : "outline"}
            className={`h-auto py-6 px-4 justify-start text-left whitespace-normal ${
              showCorrectAnswer && "bg-green-600 hover:bg-green-700"
            }`}
            onClick={() => onSelectAnswer("A")}
            disabled={selectedAnswer === null && isSubmitted}
          >
            <span className="text-lg font-medium mr-4 shrink-0">A</span>
            <span className="flex-grow">{options[0]}</span>
            {selectedAnswer === "A" && (
              <Check className="ml-2 h-5 w-5 text-green-500" />
            )}
          </Button>
        )}
      </div>
      {selectedAnswer && (
        <Button
          onClick={onDeleteAnswer}
          variant="destructive"
          className="bg-red-600 hover:bg-red-700 mt-4"
        >
          回答を削除
        </Button>
      )}
    </div>
  );
};

export default function Quiz({
  questions: initialQuestions,
  clearPDF,
  pdf_name: initialPdfName,
}: QuizProps) {
  const [answers, setAnswers] = useState<(string | null)[]>(
    Array(initialQuestions.length).fill(null)
  );

  const [isSubmitted, setIsSubmitted] = useState(false);
  const [pdfName, setPdfName] = useState<string>(initialPdfName);
  const [questions, setQuestions] = useState<Question[]>(initialQuestions); // 質問データを状態として管理

  // 回答選択ハンドラー
  const handleSelectAnswer = (index: number, answer: string) => {
    if (!isSubmitted) {
      const newAnswers = [...answers];
      newAnswers[index] = answer;
      setAnswers(newAnswers);
    }
  };

  const handleDeleteAnswer = (index: number) => {
    const newAnswers = [...answers];
    newAnswers[index] = null; // Delete the answer
    setAnswers(newAnswers);

    const updatedQuestions = [...questions];
    updatedQuestions[index] = {
      ...updatedQuestions[index],
      option: [""], // Set the option to an array with an empty string
    };
    setQuestions(updatedQuestions); // Update the questions state
  };

  const handleSubmit = async () => {
    setIsSubmitted(true);
    try {
      const response = await fetch("/api/qa", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          questions,
          userAnswers: answers,
          pdf_name: pdfName,
        }),
      });

      if (!response.ok) {
        throw new Error("Failed to submit quiz results.");
      }

      mutate("/api/qa", {
        questions,
        userAnswers: answers,
        pdf_name: pdfName,
      });

      console.log("Quiz results submitted successfully.");
    } catch (error) {
      console.error(error);
    }
  };

  const handleReset = () => {
    setAnswers(Array(questions.length).fill(null));
    setIsSubmitted(false);
  };

  return (
    <div className="min-h-screen bg-background text-foreground">
      <main className="container mx-auto px-4 py-12 max-w-4xl">
        <h1 className="text-3xl font-bold mb-8 text-center text-foreground">
          <input
            type="text"
            value={pdfName}
            onChange={(e) => setPdfName(e.target.value)}
            className="text-center border-b-2 text-xl font-bold"
          />
        </h1>
        <div className="space-y-8">
          {!isSubmitted &&
            questions.map((question, index) => (
              <QuestionCard
                key={index}
                question={question}
                selectedAnswer={answers[index]}
                onSelectAnswer={(answer) => handleSelectAnswer(index, answer)}
                onDeleteAnswer={() => handleDeleteAnswer(index)}
                isSubmitted={isSubmitted}
                showCorrectAnswer={false}
              />
            ))}

          {isSubmitted && (
            <QuizReview
              questions={questions}
              userAnswers={answers.map((answer) => answer ?? "")}
            />
          )}

          <div className="flex justify-center space-x-4 pt-4">
            {!isSubmitted ? (
              <Button
                onClick={handleSubmit}
                disabled={answers.some((answer) => answer === null)}
                className="bg-primary hover:bg-primary/90"
              >
                送信
              </Button>
            ) : (
              <>
                <Button
                  onClick={handleReset}
                  variant="outline"
                  className="bg-muted hover:bg-muted/80 w-full"
                >
                  <RefreshCw className="mr-2 h-4 w-4" /> Reset Quiz
                </Button>
                <Button
                  onClick={clearPDF}
                  className="bg-primary hover:bg-primary/90 w-full"
                >
                  <FileText className="mr-2 h-4 w-4" /> Try Another PDF
                </Button>
              </>
            )}
          </div>
        </div>
      </main>
    </div>
  );
}
```

選択すると、「回答を削除」するボタンが表示されるようになりました。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_18.21.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/29af80b7-7565-0521-70d8-453791a76a7a.png)

クリックすると、下記のようになります。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_18.23.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/a6084d74-4f90-2ca2-3188-9a7680f9f2b3.png)

そのまま削除してしまう仕様もありですが、やっぱりこの質問に対しては、編集をかけたいという方の為に、空白にしておきます。

後ほど、「空白で送信したQ&Aは、Wikiだるまのインプットからは除外されます。」という但し書きは加えるとして、ここから編集できるように修正を加えていきます。

## 編集機能をつくる

結論は以下です。

```jsx
// components/quiz.tsx

import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Check, X, RefreshCw, FileText, Edit } from "lucide-react";
import QuizReview from "./quiz-overview";
import { Question } from "@/lib/schemas";
import useSWR, { mutate } from "swr";

type QuizProps = {
  questions: Question[];
  clearPDF: () => void;
  pdf_name: string; // title を pdf_name に変更
};

const QuestionCard: React.FC<{
  question: Question;
  selectedAnswer: string | null;
  onSelectAnswer: (answer: string) => void;
  onDeleteAnswer: () => void;
  onEditOption: (id: string, newOption: string) => void;
  isSubmitted: boolean;
  showCorrectAnswer: boolean;
}> = ({
  question,
  selectedAnswer,
  onSelectAnswer,
  onDeleteAnswer,
  onEditOption,
  isSubmitted,
  showCorrectAnswer,
}) => {
  const [editingOption, setEditingOption] = useState<boolean>(false);
  const [newOption, setNewOption] = useState<string>(question.option);

  useEffect(() => {
    if (editingOption && (selectedAnswer === null || selectedAnswer === "")) {
      setNewOption("");
    }
  }, [editingOption, selectedAnswer]);

  const handleEditClick = () => {
    setEditingOption(true);
  };

  const handleSaveOption = () => {
    onEditOption(question.id, newOption);
    setEditingOption(false);
  };

  const handleCancelEdit = () => {
    setEditingOption(false);
    setNewOption(question.option);
  };

  return (
    <div className="space-y-6">
      <h2 className="text-lg font-semibold leading-tight">
        {question.question}
      </h2>
      <div className="grid grid-cols-1 gap-4">
        <div className="flex items-center justify-between">
          {editingOption ? (
            <div className="flex items-center space-x-2">
              <input
                type="text"
                value={newOption}
                onChange={(e) => setNewOption(e.target.value)}
                className="border p-2 rounded"
              />
              <Button
                variant="outline"
                onClick={handleSaveOption}
                className="ml-2"
              >
                保存
              </Button>
              <Button
                variant="outline"
                onClick={handleCancelEdit}
                className="ml-2"
              >
                キャンセル
              </Button>
            </div>
          ) : (
            <Button
              variant={
                selectedAnswer === question.option ? "secondary" : "outline"
              }
              className={`h-auto py-6 px-4 justify-start text-left whitespace-normal flex items-center w-full`}
              onClick={() => onSelectAnswer(question.option)}
              disabled={selectedAnswer === null && isSubmitted}
            >
              <span className="text-lg font-medium shrink-0 w-8">A</span>
              <span className="flex-grow text-left">{question.option}</span>
              {selectedAnswer === question.option && (
                <Check className="h-5 w-5 text-green-500 ml-2" />
              )}
              {/* 編集アイコン */}
              {!isSubmitted && !editingOption && (
                <div
                  onClick={handleEditClick} // 編集モードに切り替える
                  className="ml-4 cursor-pointer"
                >
                  <Edit className="h-5 w-5 text-blue-500" />
                </div>
              )}
            </Button>
          )}
        </div>
      </div>

      {selectedAnswer && (
        <Button
          onClick={onDeleteAnswer}
          variant="destructive"
          className="bg-red-600 hover:bg-red-700 mt-4"
        >
          回答を削除
        </Button>
      )}
    </div>
  );
};

export default function Quiz({
  questions: initialQuestions,
  clearPDF,
  pdf_name: initialPdfName,
}: QuizProps) {
  const [answers, setAnswers] = useState<(string | null)[]>(
    Array(initialQuestions.length).fill(null)
  );

  const [isSubmitted, setIsSubmitted] = useState(false);
  const [pdfName, setPdfName] = useState<string>(initialPdfName);
  const [questions, setQuestions] = useState<Question[]>(initialQuestions); // 質問データを状態として管理

  // 回答選択ハンドラー
  const handleSelectAnswer = (index: number, answer: string) => {
    if (!isSubmitted) {
      const newAnswers = [...answers];
      newAnswers[index] = answer;
      setAnswers(newAnswers);
    }
  };

  const handleDeleteAnswer = (index: number) => {
    const newAnswers = [...answers];
    newAnswers[index] = null; // Delete the answer
    setAnswers(newAnswers);

    const updatedQuestions = [...questions];
    updatedQuestions[index] = {
      ...updatedQuestions[index],
      option: "", // Set the option to an empty string
    };
    setQuestions(updatedQuestions); // Update the questions state
  };

  const handleSubmit = async () => {
    setIsSubmitted(true);
    try {
      const response = await fetch("/api/qa", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          questions,
          userAnswers: answers,
          pdf_name: pdfName,
        }),
      });

      if (!response.ok) {
        throw new Error("Failed to submit quiz results.");
      }

      mutate("/api/qa", {
        questions,
        userAnswers: answers,
        pdf_name: pdfName,
      });

      console.log("Quiz results submitted successfully.");
    } catch (error) {
      console.error(error);
    }
  };

  const handleReset = () => {
    setAnswers(Array(questions.length).fill(null));
    setIsSubmitted(false);
  };

  const handleEditOption = (id: string, newOption: string) => {
    const updatedQuestions = [...questions];
    const index = updatedQuestions.findIndex((q) => q.id === id); // Find the question by id
    if (index !== -1) {
      updatedQuestions[index].option = newOption; // Update the option
      setQuestions(updatedQuestions);
    }
  };

  return (
    <div className="min-h-screen bg-background text-foreground">
      <main className="container mx-auto px-4 py-12 max-w-4xl">
        <h1 className="text-3xl font-bold mb-8 text-center text-foreground">
          <input
            type="text"
            value={pdfName}
            onChange={(e) => setPdfName(e.target.value)}
            className="text-center border-b-2 text-xl font-bold"
          />
        </h1>
        <h2 className="text-2xl font-bold mb-8  text-foreground">
          生成されたQ&Aを確認し、1つずつ回答をタップしてください
          <br />
          回答を削除し、空白で送信されたQ&Aは、インプットから除外されます。
        </h2>
        <div className="space-y-8">
          {!isSubmitted &&
            questions.map((question, index) => (
              <QuestionCard
                key={index}
                question={question}
                selectedAnswer={answers[index]}
                onSelectAnswer={(answer) => handleSelectAnswer(index, answer)}
                onDeleteAnswer={() => handleDeleteAnswer(index)}
                onEditOption={handleEditOption} // Pass the edit handler
                isSubmitted={isSubmitted}
                showCorrectAnswer={false}
              />
            ))}

          {isSubmitted && (
            <QuizReview
              questions={questions}
              userAnswers={answers.map((answer) => answer ?? "")}
            />
          )}

          <div className="flex justify-center space-x-4 pt-4">
            {!isSubmitted ? (
              <Button
                onClick={handleSubmit}
                disabled={answers.some((answer) => answer === null)}
                className="bg-primary hover:bg-primary/90"
              >
                送信
              </Button>
            ) : (
              <>
                <Button
                  onClick={handleReset}
                  variant="outline"
                  className="bg-muted hover:bg-muted/80 w-full"
                >
                  <RefreshCw className="mr-2 h-4 w-4" />
                  リセット
                </Button>
                <Button
                  onClick={clearPDF}
                  variant="outline"
                  className="bg-muted hover:bg-muted/80 w-full"
                >
                  <FileText className="mr-2 h-4 w-4" />
                  PDFをダウンロード
                </Button>
              </>
            )}
          </div>
        </div>
      </main>
    </div>
  );
}
```

するとこんな感じになります。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_19.26.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/8083565a-6529-61f9-49d5-fc249429ba0c.png)

編集ボタンを押すとこうなります。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_19.28.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/5196e250-7c7a-3bff-0e69-23c5400c2d35.png)

これらを全部チェックしたら、送信ボタンを押下できるようになりますので、AIエージェントの完成です。
ここまでの内容をセーブしておきます。

```jsx
yarn build
git checkout -b fourth-story
git add . ; git commit -m "fourth-story" ; git push
```

## AIスコアリングに繋ぐ

これらのインプットを元に、右下の吹き出しから入力すると、OpenAIのAPI経由でAIがスコアリングし、適切な回答をしてくれるようになります。

![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-12-25_19.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970283/2e01ff3f-56c7-82a2-5134-a90c0f1d39a3.png)

次回は、Q&Aの生成数を４つから自由に変動できるようにしたいと思います。続きは[弊社ホームページ](https://www.bletainasus.com/)にて。

それではまたいつか会う日まで、ごきげんよう🍀