0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-25

本日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

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

// 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設定変更用。

// 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

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

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

%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

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

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

削除機能をつくる

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

// 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を表示しているコンポーネントを修正します。

// 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

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

%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

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

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

編集機能をつくる

結論は以下です。

// 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

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

%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

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

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

次回は、Q&Aの生成数を4つから自由に変動できるようにしたいと思います。続きは弊社ホームページにて。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?