- Published on
Tạo App Xem Tarot với Gemini API chỉ trong 15'?
Chuyện là tuy hơi có tuổi nhưng mình vẫn tự tin mình là GenZ mà GenZ 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:
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.
- 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.
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
- 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
- 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.
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)
- Chọn
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.
End.
Happy coding!!! 👩🏼💻
References:
Author: bunhere.com
I am always looking for feedback on my writing, so please let me know what you think. ❤️