本日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はこんな感じでした。
英語を日本語にするのと、エージェントの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;
結果はこんな感じになります。
PDFを貼付し、Q&A生成を行います。
ここで失敗する方は、第一話をご参照いただけますと上手くいくと思います。
内容を読み取って、Q&Aを4つ生成してくれました。
この画面にて、生成された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>
);
}
選択すると、「回答を削除」するボタンが表示されるようになりました。
クリックすると、下記のようになります。
そのまま削除してしまう仕様もありですが、やっぱりこの質問に対しては、編集をかけたいという方の為に、空白にしておきます。
後ほど、「空白で送信した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>
);
}
するとこんな感じになります。
編集ボタンを押すとこうなります。
これらを全部チェックしたら、送信ボタンを押下できるようになりますので、AIエージェントの完成です。
ここまでの内容をセーブしておきます。
yarn build
git checkout -b fourth-story
git add . ; git commit -m "fourth-story" ; git push
AIスコアリングに繋ぐ
これらのインプットを元に、右下の吹き出しから入力すると、OpenAIのAPI経由でAIがスコアリングし、適切な回答をしてくれるようになります。
次回は、Q&Aの生成数を4つから自由に変動できるようにしたいと思います。続きは弊社ホームページにて。
それではまたいつか会う日まで、ごきげんよう🍀