Primero, instala Next.js en tu proyecto.
shellbashCopiar códigonpx create-next-app@latest my-chatbot --tscd my-chatbot
Instala las dependencias necesarias:
shellbashCopiar códigonpm install firebase @heroicons/react
Para ello debemos ir al siguiente link: https://firebase.google.com/ e iniciar sesión con una cuenta de Gmail.
Luego seleccionaremos la opción Go To Console (arriba a la derecha) que nos llevará a la vista donde tenemos nuestros proyectos de Firebase, allí seleccionaremos la opción Agregar Proyecto:
Esa opción nos llevará a la siguiente vista, donde crearemos el proyecto:
El paso 1 de la creación de un proyecto en Firebase nos consultará el nombre de nuestro proyecto:
Luego, deshabilitaremos la configuración de Analytics ya que para efectos del taller no será necesario y le damos en Continuar:
Luego, esperamos mientras se crea el proyecto (puede tardar unos minutos):
Una vez termina su creación se ve de la siguiente manera, allí daremos click en Continuar:
Y así se verá nuestro dashboard del nuevo proyecto
En dicho dashboard, vamos a seleccionar el botón con los símbolos </> , ésto nos permitirá crear las credenciales para conectarlo con nuestra app web, y allí nos pedirá registrar el nombre de nuestra aplicación web y haremos click en el botón Registrar app:
El siguiente paso, nos mostrará sobre la instalación de firebase (sin embargo no será necesario por que nuestro proyecto base ya tiene firebase instalado) y luego nos arrojará las credenciales de nuestra app.
Por ultimo vamos a agregar las credenciales en el archivo .env en la raíz de tu proyecto donde las usaremos mas adelante.
plain textenvCopiar códigoNEXT_PUBLIC_FIREBASE_API_KEY=your-api-keyNEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-auth-domainNEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-idNEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-storage-bucketNEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-idNEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
En la consola de Firebase, iremos a la opción que dice Extensions dentro del menú Compilación.
Al dar click en Extensions nos muestra las diferentes extensiones que podemos usar, en nuestro caso, buscamos la que dice Build Chatbot with the Gemini API y presionaremos Instalar
Ésto nos redirigirá a la siguiente vista, en donde seleccionaremos una opción que se muestra más abajo llamada Actualizar proyecto para continuar:
Allí seleccionaremos la Cuenta de Facturación creada en el primer paso del workshop, seleccionar dicha opción y luego Continuar.
Colocaremos un presupuesto de 5 USD y presionamos continuar:
Y seleccionamos la opción Comprar
Deberá aparecerte el siguiente mensaje, podremos cerrarla y continuar:
Deberemos Habilitar las siguientes opciones
Luego, se habilitará el botón de Siguiente para continuar.
Nuevamente presionamos Siguiente:
Y ahora nos pide configurar nuestra extensión:
Allí cambiaremos la siguiente información:
Luego presionaremos la opción de Instalar la extensión.
Allí esperaremos unos minutos mientras se instala…
Y luego haremos click en Comenzar
Aquí veremos cómo podemos probar dicha extensión en nuestro proyecto web, sin embargo, nosotros/as ya lo tomamos en cuenta en pasos anteriores.
Para probar que nuestra extensión funciona correctamente, podemos ir a firestore, prueba presionando en el botón de Agregar Documento y agregarle al documento un parámetro llamado prompt con un saludo cómo: “Hola cómo estás?” y verás que en cuestión de segundos Gemini generará la respuesta.
Una vez vista la respuesta ya estamos listos para crear nuestro código fuente!
Organiza tu proyecto con la siguiente estructura:
lualuaCopiar códigomy-chatbot/├── public/│ ├── bot.png│ └── man.png├── src/│ ├── app/│ │ ├── chat/│ │ │ └── page.tsx│ │ └── page.tsx│ ├── components/│ │ └── FloatingChatbot.tsx│ ├── hooks/│ │ └── useChat.ts│ └── lib/│ └── firebaseConfig.ts├── .env.local├── package.json└── tailwind.config.js
Crea el archivo firebaseConfig.ts en src/lib:
typescripttypescriptCopiar código// src/lib/firebaseConfig.tsimport { initializeApp } from 'firebase/app';import { getFirestore } from 'firebase/firestore';import { getAuth } from 'firebase/auth';const firebaseConfig = {apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,};const app = initializeApp(firebaseConfig);const db = getFirestore(app);const auth = getAuth(app);export { db, auth };
Crea el archivo useChat.ts en src/hooks:
typescripttypescriptCopiar código// src/hooks/useChat.tsimport { useState, useEffect } from 'react';import { db } from '../lib/firebaseConfig';import { collection, addDoc, query, orderBy, onSnapshot, serverTimestamp, doc } from 'firebase/firestore';const useChat = (userId: string, conversationId: string, userProfilePicUrl: string, botProfilePicUrl: string) => {const [message, setMessage] = useState('');const [chat, setChat] = useState([]);const [isTyping, setIsTyping] = useState(false);useEffect(() => {const q = query(collection(db, 'conversations', conversationId, 'messages'),orderBy('timestamp', 'asc'));const unsubscribe = onSnapshot(q, (querySnapshot) => {const messages = querySnapshot.docs.map((doc) => doc.data());setChat(messages);});return () => unsubscribe();}, [conversationId]);const handleSendMessage = async () => {if (message.trim()) {const userMessage = {prompt: message,sender: userId,timestamp: serverTimestamp(),profilePicUrl: userProfilePicUrl,};setChat((prevChat) => [...prevChat, userMessage]);setMessage('');setIsTyping(true);try {await addDoc(collection(db, 'conversations', conversationId, 'messages'), userMessage);const botResponseRef = await addDoc(collection(db, 'generate'), {prompt: message,});const unsubscribe = onSnapshot(doc(db, 'generate', botResponseRef.id), async (snap) => {if (snap.get('response')) {const botMessage = {prompt: snap.get('response'),sender: 'bot',timestamp: serverTimestamp(),profilePicUrl: botProfilePicUrl,};await addDoc(collection(db, 'conversations', conversationId, 'messages'), botMessage);setChat((prevChat) => [...prevChat, botMessage]);setIsTyping(false);unsubscribe();}});} catch (error) {console.error("Error adding document: ", error);}}};const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {if (e.key === 'Enter') {handleSendMessage();}};return {message,setMessage,chat,isTyping,handleSendMessage,handleKeyDown,};};export default useChat;
Crea el archivo page.tsx en src/app/chat:
typescripttsxCopiar código// src/app/chat/page.tsx"use client";import useChat from '../../hooks/useChat';import Image from 'next/image';const USER_ID = '1';const CONVERSATION_ID = '1';const USER_PROFILE_PIC_URL = '/man.png';const BOT_PROFILE_PIC_URL = '/bot.png';export default function Chat() {const {message,setMessage,chat,isTyping,handleSendMessage,handleKeyDown,} = useChat(USER_ID, CONVERSATION_ID, USER_PROFILE_PIC_URL, BOT_PROFILE_PIC_URL);return (<div className="min-h-screen flex flex-col items-center bg-gray-50 p-4"><div className="w-full max-w-2xl bg-white shadow-lg rounded-lg p-6 flex flex-col"><div className="flex-1 overflow-y-auto mb-4">{chat.map((msg, index) => (<div key={index} className={`flex items-end mb-2 ${msg.sender === USER_ID ? 'justify-end' : 'justify-start'}`}><Image src={msg.profilePicUrl} alt="profile picture" width={40} height={40} className="rounded-full mr-2" /><div className={`p-2 rounded-lg ${msg.sender === USER_ID ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>{msg.prompt}</div></div>))}{isTyping && (<div className="flex items-end mb-2 justify-start"><Image src={BOT_PROFILE_PIC_URL} alt="bot typing" width={40} height={40} className="rounded-full mr-2" /><div className="p-2 rounded-lg bg-gray-200 text-gray-800">El bot está escribiendo...</div></div>)}</div><div className="flex"><inputtype="text"value={message}onChange={(e) => setMessage(e.target.value)}onKeyDown={handleKeyDown}placeholder="Escribe tu mensaje..."className="flex-1 border border-gray-300 p-2 rounded-lg"/><buttononClick={handleSendMessage}className="ml-2 bg-blue-600 text-white p-2 rounded-lg hover:bg-blue-700 transition">Enviar</button></div></div></div>);}
Crea el archivo FloatingChatbot.tsx en src/components:
typescripttsxCopiar código// src/components/FloatingChatbot.tsx"use client";import { useState, useEffect, useRef } from 'react';import { ChatBubbleLeftEllipsisIcon, XMarkIcon } from '@heroicons/react/24/solid';import useChat from '../hooks/useChat';import Image from 'next/image';const USER_ID = '1';const CONVERSATION_ID = '1';const USER_PROFILE_PIC_URL = '/man.png';const BOT_PROFILE_PIC_URL = '/bot.png';const FloatingChatbot = () => {const [isOpen, setIsOpen] = useState(false);const {message,setMessage,chat,isTyping,handleSendMessage,handleKeyDown,} = useChat(USER_ID, CONVERSATION_ID, USER_PROFILE_PIC_URL, BOT_PROFILE_PIC_URL);const messagesEndRef = useRef<HTMLDivElement>(null);const toggleChat = () => {setIsOpen(!isOpen);};const scrollToBottom = () => {messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });};useEffect(() => {if (isOpen) {scrollToBottom();}}, [isOpen, chat]);return (<div><div className="fixed bottom-4 right-4 z-50"><buttononClick={toggleChat}className="bg-blue-600 text-white p-4 rounded-full shadow-lg hover:bg-blue-700 transition">{isOpen ? <XMarkIcon className="h-6 w-6" /> : <ChatBubbleLeftEllipsisIcon className="h-6 w-6" />}</button></div>{isOpen && (<div className="fixed bottom-16 right-4 z-50 bg-white border border-gray-300 rounded-lg shadow-lg w-80 h-96"><div className="flex flex-col h-full"><div className="flex items-center justify-between p-4 border-b"><h2 className="text-lg font-bold">Chatbot</h2><buttononClick={toggleChat}className="text-gray-500 hover:text-gray-700"><XMarkIcon className="h-6 w-6" /></button></div><div className="flex-1 p-4 overflow-y-auto">{chat.map((msg, index) => (<div key={index} className={`flex items-end mb-2 ${msg.sender === USER_ID ? 'justify-end' : 'justify-start'}`}><Image src={msg.profilePicUrl} alt="profile picture" width={30} height={30} className="rounded-full mr-2" /><div className={`p-2 rounded-lg ${msg.sender === USER_ID ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>{msg.prompt}</div></div>))}{isTyping && (<div className="flex items-end mb-2 justify-start"><Image src={BOT_PROFILE_PIC_URL} alt="bot typing" width={30} height={30} className="rounded-full mr-2" /><div className="p-2 rounded-lg bg-gray-200 text-gray-800">El bot está escribiendo...</div></div>)}<div ref={messagesEndRef} /></div><div className="flex p-2 border-t"><inputtype="text"value={message}onChange={(e) => setMessage(e.target.value)}onKeyDown={handleKeyDown}placeholder="Escribe tu mensaje..."className="flex-1 border border-gray-300 p-2 rounded-lg"/><buttononClick={handleSendMessage}className="ml-2 bg-blue-600 text-white p-2 rounded-lg hover:bg-blue-700 transition">Enviar</button></div></div></div>)}</div>);};export default FloatingChatbot;
Actualiza page.tsx en src/app:
typescripttsxCopiar código// src/app/page.tsximport Image from 'next/image';import Link from 'next/link';import FloatingChatbot from '../components/FloatingChatbot';export default function Home() {return (<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50"><div className="bg-white shadow-lg rounded-lg p-8 max-w-lg text-center"><h1 className="text-4xl font-bold text-gray-900 mb-4">¡Bienvenidos al Taller de Chatbots Inteligentes!</h1><p className="text-gray-700 mb-6">Aprende a crear un chatbot inteligente utilizando Google Cloud, Gemini, Next.js y Firebase.</p><Imagesrc="/images/chatbot-welcome.png"alt="Welcome to the Chatbot Workshop"width={400}height={250}className="mx-auto mb-6"/><Link href="/chat" className="inline-block bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition">Comenzar</Link></div><FloatingChatbot /></div>);}
Con estos pasos, has creado un chatbot inteligente utilizando Next.js, Firebase y la extensión Gemini. El chatbot incluye una página de chat dedicada y un componente flotante que permite a los usuarios interactuar con el bot desde cualquier página.
Recuerda:
¡Felicidades! si tu proyecto luce como la imagen anterior, has completado el proyecto. Si tienes alguna pregunta o necesitas más ayuda, no dudes en contactarme.