阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

在本教程中,您將學習如何建立人工智慧驅動的測驗應用程式,該應用程式使用戶能夠選擇主題、回答與該主題相關的問題,並在完成測驗後立即收到分數。

此測驗的問題將使用 OpenAI API 動態生成,方法是提供以所需 JSON 格式傳回問題的特定提示。在建立此應用程式時,您還將學習如何將 OpenAI 與您的軟體應用程式整合。

動圖

什麼是緯度?

Latitude AI 是一個開源提示工程平台,可讓您輕鬆建置、測試和部署由大型語言模型 (LLM) 提供支援的功能。該平台使團隊能夠建立功能強大且智慧的人工智慧應用程式。

您可以加入我們的等候名單,以了解有關 Latitude AI 的更多資訊。請隨意與團隊聯繫,了解我們如何使用人工智慧解決各種挑戰。

https://github.com/latitude-dev/latitude

https://ai.latitude.so/ 加入候補名單 ⭐️


使用 Next.js 建立測驗應用程式

在本節中,您將學習如何建立測驗應用程式的介面。該應用程式分為三個頁面: Home PageTest PageScore Page

主頁顯示所有可用主題。測試頁面呈現一個問題並提供選項清單供使用者選擇正確答案。最後,分數頁面顯示使用者的分數。

透過執行以下程式碼片段來建立一個新的 Next.js Typescript 專案:

npx create-next-app ai-quiz-app

types.d.ts檔案新增至專案的根目錄,以定義應用程式測驗問題的資料結構。

interface Question {
    question: string;
    options: string[];
    answer: string;
    id: number;
}

接下來,在 Next.js app資料夾中建立一個包含util.ts檔案的lib資料夾:

//👇🏻 topics list
export const firstTopics = [
    { id: "AI", name: "AI Questions" },
    { id: "Python", name: "Python Questions" },
    { id: "JavaScript", name: "JavaScript Questions" },
];

//👇🏻 topics list
export const secondTopics = [
    { id: "CSS", name: "CSS Questions" },
    { id: "HTML", name: "HTML Questions" },
    { id: "UI Design", name: "UI Design Questions" },
];
//👇🏻 capitalize the first letter of each word
export const capitalize = (str: string): string => {
    str = str.replace(/%20/g, " ");
    if (str.length === 0) {
        return str;
    }
    return str.charAt(0).toUpperCase() + str.slice(1) + " Questions";
};

firstTopicssecondTopics陣列包含應用程式中可用的主題列表,並且capitalize函數接受字串作為其參數並將句子的第一個字母大寫。

首頁

將下面的程式碼片段複製到app/page.tsx檔案中:

"use client";
import { firstTopics, secondTopics } from "./lib/util";
import { useRouter } from "next/navigation";

export default function Home() {
    const router = useRouter();

    const handleConfirmClick = (id: string) => {
        const result = confirm(`Are you sure you want to take the ${id} test?`);
        if (result) {
            router.push(`/test/${id}`);
        } else {
            alert(`You have cancelled the ${id} test`);
        }
    };

    return (
        <main className='w-full min-h-screen flex flex-col items-center justify-center'>
            <h2 className='text-4xl font-bold text-blue-600'>Take Tests</h2>
            <p className='text-lg text-gray-500 mb-5'>
                Select a topic, take tests and get your results instantly
            </p>
            <div className='px-4'>
                <section className='w-full flex items-center space-x-5 mb-4'>
                    {firstTopics.map((topic) => (
                        <button
                            key={topic.id}
                            className={`bg-blue-500 text-white px-5 py-3 text-xl rounded-md`}
                            onClick={() => handleConfirmClick(topic.id)}
                        >
                            {topic.name}
                        </button>
                    ))}
                </section>

                <section className='w-full flex items-center space-x-5'>
                    {secondTopics.map((topic) => (
                        <button
                            key={topic.id}
                            className={`bg-blue-500 text-white px-5 py-3 text-xl rounded-md`}
                            onClick={() => handleConfirmClick(topic.id)}
                        >
                            {topic.name}
                        </button>
                    ))}
                </section>
            </div>
        </main>
    );
}

主頁顯示所有可用主題,並在使用者點擊主題連結時將其引導至測試頁面。

螢幕錄製

測試頁

透過在test/[id]目錄中新增page.tsx檔案來建立測試頁面。將下面的程式碼片段複製到test/[id]/page.tsx檔案中:

"use client";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { capitalize } from "@/app/lib/util";

export default function Test() {
    //👇🏻 array of questions
    const [questions, setQuestions] = useState<Question[]>([]);
    //👇🏻 loading state
    const [loading, setLoading] = useState<boolean>(true);
    //👇🏻 total user's score
    const [userScore, setUserScore] = useState<number>(0);
    //👇🏻 tracks each question in the array
    const [count, setCount] = useState<number>(0);
    //👇🏻 holds the quiz topic
    const { id } = useParams<{ id: string }>();
    const router = useRouter();

    const handleSelectAnswer = (selectedAnswer: string) => {
        //👇🏻 Update the score
        setUserScore((prev) =>
            selectedAnswer === questions[count].answer ? prev + 1 : prev
        );

        //👇🏻 Check if it's the last question
        if (count < questions.length - 1) {
            //👇🏻 Move to the next question
            setCount((prev) => prev + 1);
        } else {
            //👇🏻  If it's the last question, navigate to the score page after the score has updated
            setTimeout(() => {
                router.push(
                    "/score?score=" +
                        (selectedAnswer === questions[count].answer
                            ? userScore + 1
                            : userScore)
                );
            }, 0); // 👈🏼 Ensure the score is updated before navigating
        }
    };

    if (loading) {
        return <h3 className='font-semibold text-2xl mb-3'>Loading...</h3>;
    }

    return (
        <main className='w-full min-h-screen p-6 flex flex-col items-center justify-center'>
            <h2 className='font-bold text-3xl mb-4 text-blue-500'>
                {capitalize(id)}
            </h2>
            <h3 className='font-semibold text-2xl mb-3'>
                Question: {count + 1} of {questions.length}
            </h3>

            <h3 className='text-xl mb-4'>{questions[count]?.question}</h3>

            <div className='flex flex-col lg:w-1/3 mb-6'>
                {questions[count]?.options.map((option, index) => (
                    <button
                        className='p-4 bg-[#EEEEEE]  
                rounded-xl mb-6 min-w-[200px] hover:bg-[#EF5A6F] hover:text-white text-lg'
                        key={index}
                        onClick={() => handleSelectAnswer(option)}
                    >
                        {option}
                    </button>
                ))}
            </div>
        </main>
    );
}

從上面的程式碼片段來看:

  • questions狀態保存所選主題的所有問題,而count狀態用於瀏覽問題陣列,允許使用者回答每個問題。

  • userScore狀態儲存使用者完成測試後的總分。

  • 然後,使用者的總分作為參數傳遞到分數頁面。

螢幕錄製

分數頁面

在 Next.js 應用程式資料夾中建立一個包含page.tsx檔案的score資料夾,並將程式碼片段複製到該檔案:

"use client";
import Link from "next/link";
import { useSearchParams } from "next/navigation";

export default function Score() {
    const searchParams = useSearchParams();
    const score = searchParams.get("score");

    if (!score) {
        return (
            <main className='p-4 min-h-screen w-full flex flex-col items-center justify-center'>
                <h2 className='text-2xl font-semibold'>Score</h2>
                <Link href='/' className='bg-blue-500 p-4 text-blue-50 rounded '>
                    Go Home
                </Link>
            </main>
        );
    }

    return (
        <main className='p-4 min-h-screen w-full flex flex-col items-center justify-center'>
            <h2 className='text-2xl font-semibold'>Score</h2>

            <p className='text-lg text-center mb-4'>
                You got {score} out of 10 questions correct.
            </p>

            <h1 className='font-extrabold text-5xl text-blue-500 mb-3'>
                {Number(score) * 10}%
            </h1>

            <Link href='/' className='bg-blue-500 p-4 text-blue-50 rounded '>
                Go Home
            </Link>
        </main>
    );
}

從上面的程式碼片段來看,「分數」頁面接受使用者的總分並以百分比顯示結果。

結果


如何將 OpenAI 整合到您的 Next.js 應用程式中

OpenAI允許我們將各種大型語言模型 (LLM)(例如 GPT-3 和 GPT-4)整合到我們的應用程式中以建立智慧功能。這些模型可以執行廣泛的自然語言處理任務,包括文字生成、翻譯、摘要等。在本部分中,您將學習如何使用 OpenAI 以所需格式產生測驗問題。

在繼續之前,請造訪OpenAI 開發者平台並建立一個新的金鑰。

開放人工智慧平台

建立一個.env.local檔案並將新建立的金鑰複製到該檔案中。

OPENAI_API_KEY=<your_API_key>

透過在終端機中執行以下命令來安裝OpenAI JavaScript SDK

npm install openai

接下來,我們建立一個 API 端點,以根據使用者選擇的主題從 OpenAI 檢索 AI 產生的問題。

在 Next.js 應用程式目錄中新增一個包含route.ts檔案的api資料夾。

cd app
mkdir api && cd api
touch route.ts

將下面的程式碼片段複製到api/route.ts檔案中。它接受包含來自客戶端的選定主題的 POST 請求。

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
    const { topic } = await req.json();

    console.log({ topic }); 👉🏻 // topic is JavaScript, UI Design, etc

    return NextResponse.json({ message: "Fetch complete" }, { status: 200 });
}

在測試頁面中,新增一個useEffect掛鉤,該掛鉤將 POST 請求傳送到 API 端點並傳回問題陣列:

const fetchQuestions = useCallback(async () => {
    const request = await fetch(`/api`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({ topic: id }),
    });
    const data = await request.json();
    setQuestions(data.questions);
    setLoading(false);
}, [id]);

useEffect(() => {
    fetchQuestions();
}, [fetchQuestions]);

libs資料夾中新增sample.json檔案並將以下程式碼片段複製到其中:

{
    "questions": [
        {
            "id": 1,
            "question": "What is the capital of France?",
            "options": ["Paris", "London", "Berlin", "Madrid"],
            "answer": "Paris"
        },
        {
            "id" : 2,
            "question": "What is the capital of Germany?",
            "options": ["Paris", "London", "Berlin", "Madrid"],
            "answer": "Berlin"
        }
    ]
}

sample.json檔案定義了 OpenAI 預期問題的結構。

最後,更新 API 端點以使用 OpenAI LLM 產生並傳回 JSON 格式的問題清單。

import { NextRequest, NextResponse } from "next/server";
import sampleQuestions from "@/app/lib/sample.json"
import OpenAI from "openai";

const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(req: NextRequest) {
    //👇🏻 User's selected topic
    const { topic } = await req.json();

    //👇🏻 AI prompt
        const prompt = `Generate 10 distinct questions on ${topic} and ensure they are in JSON format containing an id, topic which is ${topic}, a question attribute containing the question, an options array of 3 options, and an answer property. Please ensure that the options array is shuffled to ensure that the answer does not retain a single position.
    - Please don't make the answers too obvious and lengthy.
    - Ensure the questions are unique and not repetitive.
    - The questions should not be too simple but intermediate level.
    - Return only the JSON object containing the questions.
    You can use this as a sample: ${JSON.stringify(sampleQuestions)}
    `;

    //👇🏻 Generates the questions
    const completion = await openai.chat.completions.create({
        model: "gpt-3.5-turbo",
        messages: [
            {
                role: "user",
                content: prompt,
            },
        ],
    });

  //👇🏻 Questions result
    const aiQuestions = completion.choices[0].message.content;
    const questions = JSON.parse(aiQuestions!);

    if (questions.questions.length < 10) {
        return NextResponse.json(
            { message: "Error generating questions", questions },
            { status: 400 }
        );
    }
    //👇🏻 Returns the list of questions
    return NextResponse.json(
        { message: "Fetch complete", questions: questions.questions },
        { status: 200 }
    );
}

上面的程式碼片段建立了一個格式精確的提示,該提示使用 OpenAI 的 GPT-3 模型產生所需的問題。隨後返回生成的問題。

問題

恭喜!您已完成本教學的專案。


下一步

到目前為止,您已經學習瞭如何建立人工智慧生成的測驗應用程式。您可以透過驗證使用者身份並將其分數保存在資料庫中來改進應用程式。

透過有效的提示,您可以利用人工智慧建立智慧軟體應用。 Latitude 正在完善這項流程,以透過快速工程釋放人工智慧的全部潛力。

想要成為第一批體驗人工智慧驅動的應用程式的下一代發展的人嗎?加入我們的等候名單,成為旅程的一部分。

感謝您的閱讀!


原文出處:https://dev.to/latitude/building-an-ai-powered-quiz-application-with-nextjs-and-openai-2673


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!