bunhere.com
Published on

Tạo App Xem Tarot với Gemini API chỉ trong 15'?

Viblo: Tạo App Xem Tarot với Gemini API chỉ trong 15'?

Tarot with GeminiAPI

Chuyện là tuy hơi có tuổi nhưng mình vẫn tự tin mình là GenZGenZ khi gặp khó khăn thì luôn tìm kiêm câu trả lời từ vũ trụ 🤓.

Mình được giới thiệu 1 bạn để xem Tarot nhưng vì đợi bạn đó rảnh để xem lâu quá, mà mình lại tình cờ xem được 1 cái đoạn prompt để xem Tartot trên Tiktok. Mình thử qua cả chatGPT và Gemini thì thấy output cả 2 bên đều quy về 1 vấn đề mà mình đang gặp phải Năng lượng vũ trụ dữ dội quá rồi ha 😝

Bạn nào nghiêm túc quá thì có thể bỏ nội dung này. Kệ ba cái Năng lượng vũ trụ luôn ha!

Ý tưởng

Nội dung của đoạn prompt đó là như này:

Generate 3 random numbers between 1 and 78. Next, look up the corresponding tarot cards, and their meanings. Finally, put together an overall reading for me, based on the 3 cards. Answer in Vietnamese

Còn đây là kết quả mình nhận được:

Tarot result from GeminiAPI

Chung quy cả 2 kết quả điều bảo là mình cần "nhìn nhận và lắng nghe tiếng nói bên trong".

Từ ý tưởng trên mình quyết định viết một ứng dụng thay vì random lấy 3 lá trong 78 lá bài Tarot thì mình show 78 lá bài ra (tất nhiên là show random), sau đó cho mọi người chọn ra 3 lá ứng với 3 số. Rồi điều chỉnh lại prompt gọi API lấy kết quả từ Gemini (vì cài đặt từ GeminiAPI khá đơn giản) sau đó trả về cho người dùng.

Như vậy thì mọi người có thể chủ động hơn trong việc chọn lá bài.

Triển khai

Giao diện

Techstack: Mình dùng NextJS khá nhiều dạo gần đây, vì nó dễ dàng triển khai cũng như deploy (Vercel).

Đầu tiên, thì mình cần render layout của 78 lá bài để người dùng bốc bài.

  1. Resources

Để có được UI của các lá bài Tarot mình lên Dribbble tìm, Dribbble là website mà designer hay truy cập để tìm ý tưởng.

Dribbble

Sau khi tải được bộ Tarot, mình chọn upload và truy cập nó qua S3 (bạn có thể lưu trữ ngay trong src).

Xong phần hình ảnh thì mình qua phần dữ liệu, tạo 1 bộ data của các tarot cards dưới dạng json là truy vấn.

Note: Bạn có thể bỏ qua phần này và khởi tạo 1 mảng từ 1-78 để xử lý, vì cái mình cần dùng là 3 con số được "pick" nhưng mình muốn hiển thị thêm thông tin về lá bài nên mình tạo bộ dữ liệu dạng json này.

Và tất nhiên, mình tạo dữ liệu bằng cách nhờ Gemini 🤪. Đoạn prompt mình dùng:

Create a data set of objects containing the contents of all tarot cards. The main contents of the objects are the id, the name of the card, the main and reverse meaning and further description of the cards, and help us create an additional contents for the card is src is '/[alias-name-of-the-card].png'. meaning, reverse meaning and description of content in Vietnamese.

Kết quả:

[
    {
        "id": 0,
        "name": "The Fool",
        "mainMeaning": "Khởi đầu, ngây thơ, tự phát, tinh thần tự do",
        "reverseMeaning": "Kiềm chế, liều lĩnh, mạo hiểm",
        "description": "The Fool đại diện cho khởi đầu mới, tin vào tương lai, thiếu kinh nghiệm, không biết điều gì sẽ xảy ra, may mắn người mới bắt đầu, ứng biến, và tin vào vũ trụ.",
        "src": "/the-fool.png",
        "type": "major-arcana"
    },
    {
        ...
    },
    ...
]

Full source: cards.json

  1. Random Tarot Cards

Viết function để random thứ tự hiển thị của các là bài tarot.

import { Card } from "@/types/card";

export function shuffleArray<Card>(array: Card[]): Card[] {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}

Suhffle source: src/common/shuffle.ts

Hiển thị random các lá bài tarot

import { useEffect, useState } from "react";
import tarotCards from "@/data/cards.json";
import TarotCard from "@/components/TarotCard";
import { shuffleArray } from "@/common/shuffle";
import { Card } from "@/types/card";

...
const rootCards: Card[] = tarotCards
const [cards, setCards] = useState<Card[]>(rootCards)

useEffect(() => {
    setCards(shuffleArray(rootCards)) // Call shuffleArray when loading
}, [rootCards])

const handleRefresh = () => {
    setCards(shuffleArray(rootCards)) // Call shuffleArray when refresh pick
}

return (
    <>
        {cards && cards.map((card) => (
            <TarotCard key={card.id} card={card} />
        ))}
        <button onClick={handleRefresh}>Refresh</button>
    </>
);

Page source: src/app/page.tsx

Xử lý logic

  1. Kết nối với GeminiAPI

Step 1: Truy cập aistudio.google và tạo API Key. Thêm vào file .env

Chi tiết:

NEXT_PUBLIC_S3_ENDPOINT=sAIza...ICYp

Cài đặt:

yarn add @google/generative-ai

Step 2: Chỉnh sửa lại prompt

With 3 cards: [card 1], [card 2], and [card 3]. Next, look up the corresponding tarot cards and their meanings. Finally, put together an overall reading for me based on the three cards. Answer in Vietnamese

Step 3: Xử lý code để kết nối đến GeminiAPI

Bạn có thể tìm hiểu thêm về cách sử dụng GeminiAPI với document.

billing information

Lưu ý: Bạn cần lưu ý thông tin billing để xử lý cho phù hợp. Vì mình dùng account FREE nên sẽ bị giới hạn 15 request/minute, nên mình tạo biến giới hạn việc request API MAX_CALLS_PER_MINUTE.

'use client'
import { useEffect, useState } from "react"

import tarotCards from "@/data/cards.json"
import { shuffleArray } from "@/common/shuffle"
import { Card } from "@/types/card"

import TarotCard from "@/components/TarotCard"
import DefaultCard from "@/components/DefaultCard"

import { GoogleGenerativeAI } from "@google/generative-ai"

const API_KEY = process.env.NEXT_PUBLIC_GEMINI_API_KEY || ''
const MAX_CALLS_PER_MINUTE = 10

export default function Home() {
  const rootCards: Card[] = tarotCards
  const [cards, setCards] = useState<Card[]>(rootCards)
  const [pickCards, setPickCards] = useState<number[]>([])
  const [prompt, setPrompt] = useState<string>("")
  const [result, setResult] = useState<string>("")
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [callCount, setCallCount] = useState(0)
  const [timer, setTimer] = useState<NodeJS.Timeout | null>(null)

  useEffect(() => {
    setCards(shuffleArray(rootCards))
  }, [rootCards])

  useEffect(() => {
    if (pickCards.length > 2) { // Pick enough 3 cards
      setPrompt(`With 3 cards: ${pickCards[0]}, ${pickCards[1]}, and ${pickCards[2]}. Next, look up the corresponding tarot cards and their meanings. Finally, put together an overall reading for me based on the three cards. Answer in Vietnamese`)
    }
  }, [pickCards, cards])

  const handlePickCard = (cardId: number) => {
    if (pickCards.length > 2) {
      console.log('You only can choose 3 cards')
      return
    }
    setPickCards([...pickCards, cardId])
  }

  const handleResetPick = () => {
    setCards(shuffleArray(rootCards))
    setPickCards([])
    setPrompt("")
    setResult("")
  }

  useEffect(() => {
    if (timer === null) {
      const newTimer = setInterval(() => {
        setCallCount(0)
      }, 60000) // Reset count every 60 seconds
      setTimer(newTimer)

      // Clean up timer on component unmount
      return () => clearInterval(newTimer)
    }
  }, [timer])

  const handleReadCards = async () => {
    if (prompt === "") {
      alert('Vui lòng chọn đủ 3 lá bài!')
      return
    }

    if (callCount >= MAX_CALLS_PER_MINUTE) {
      alert('Hệ thống quá tải! Vui lòng chờ 1 phút sau đó thử lại.')
      return
    }

    setCallCount(callCount + 1)
    await handleSendPromptToGemini(prompt)
  }

  const handleSendPromptToGemini = async (prompt: string) => {
    setIsLoading(true);
    try {
      const genAI = new GoogleGenerativeAI(API_KEY)
      const model = genAI.getGenerativeModel({ model: "gemini-pro" })

      const result = await model.generateContent(prompt)
      const response = result.response
      const text = response.text()

      setResult(text)
    } catch (error) {
      setResult('Failed to fetch response.')
    }
    setIsLoading(false)
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {isLoading &&
        <p className="pb-8">loading...</p>
      }
      <button className={`bg-black text-white p-4 text-center border hover:bg-white hover:text-black ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`} disabled={isLoading} onClick={handleResetPick}>Reset</button>
      {result &&
        <p className="pb-8">{result}</p>
      }
      <div className="relative flex flex-wrap">
        {cards && cards.filter((card) => !pickCards.includes(card.id)).map((card: Card) => (
          <div key={card.id} className="relative -ml-20 hover:-mt-4">
            <TarotCard card={card} handlePickCard={handlePickCard} />
          </div>
        ))}
      </div>
      <button className={`bg-black text-white p-4 text-center border hover:bg-white hover:text-black ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`} disabled={isLoading} onClick={() => handleReadCards()}>Read Cards</button>

      <DefaultCard cards={cards} pickCard={pickCards} />
    </main>
  )
}

Page source: src/app/page.tsx

Deloy

  • Tạo repo và push dự án của bạn lên Github
  • Đăng nhập vào Vercel nếu đã có tài khoản, kết nối với tài khoản Github của bạn.
    • Chọn New project
    • Chọn repo từ tài khoản Github
    • Deloy dự án (nếu failed vào phần Settings > Enviroment > Thêm biến NEXT_PUBLIC_GEMINI_API_KEY và giá trị tương ứng)

Chi tiết: Deloy Vercel For Github

Demo

Truy cập tarot1.bunhere.com cho bản demo. Bản chính thức tại tarot.bunhere.com.

Xem full source code tại đây.

Tarot | Bunhere

End.

Happy coding!!! 👩🏼‍💻

References:

Author: bunhere.com
I am always looking for feedback on my writing, so please let me know what you think. ❤️