🇺🇸 EN
Frontend
·9 min read·↗·🇺🇸 EN

Cómo crear un ChatBot con Gemini, Kendo UI y Angular 17

Portada principal del artículo: Cómo crear un ChatBot con Gemini, Kendo UI y Angular 17

Al ver tantos chats conectados con IA, un amigo me dijo una vez que esto era difícil, pero la verdad es que no lo es. Hoy quiero mostrarte lo fácil que resulta construir tu propio chat conectado con IA y con una interfaz muy cuidada usando solo unas pocas líneas de código.

Vamos a aprender cómo crear una aplicación en Angular usando la librería de Gemini e integrarla con Kendo Conversational UI para lograr una interfaz de usuario impecable.

¿Cuánto tiempo tomará? Sorprendentemente, se puede hacer muy rápido. ¡Empecemos!

Configuración del proyecto

Primero, creamos la aplicación de Angular con Angular CLI ejecutando el comando ng new conversional-with-gemini. Esto genera un proyecto de Angular nuevo y listo para trabajar.

ng new conversional-with-gemini

Entramos al directorio y generamos un servicio para el cliente de Gemini usando el generador del CLI:

ng g s services/gemini
CREATE src/app/services/gemini.service.spec.ts (404 bytes)
CREATE src/app/services/gemini.service.ts (150 bytes)

En Angular 17, como ya no se incluye el archivo de entornos por defecto, lo creamos con el comando ng generate environments.

ng generate environments

Usando Gemini

Para usar Gemini, necesitamos su SDK junto con una clave de API. Primero, instalamos el SDK de Gemini en el proyecto ejecutando el siguiente comando:

npm i @google/generative-ai

Luego, obtén tu clave de API en https://ai.google.dev/, cópiala y guarda el valor en el archivo environment.ts, de esta forma:

export const environment = {
  geminy_key: "YourAmazingKey"
};

Ya tenemos la clave y el SDK instalado. Ahora es momento de usarlos en nuestro servicio de Gemini. ¡Vamos a ello!

Usando el SDK de Gemini

Vamos a usar el SDK de Gemini dentro del servicio gemini-client. Creamos una instancia de Google AI con la clave y obtenemos el modelo (en nuestro caso 'gemini-pro'), lo que nos devuelve una instancia lista para trabajar.

En nuestro caso, queremos consultar el modelo a partir de un prompt. Para lo que necesito en particular, quiero que actúe como un desarrollador de Angular con experiencia en Kendo UI para Angular.

Así que mi prompt será el siguiente:

You are an expert in the Kendo UI library for Angular, provide a real-world scenario and how this product, helps to solve, with angular code examples and links for resources

El código queda de la siguiente manera:

import { Injectable } from '@angular/core';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { environment } from '../../environments/environment';
 
@Injectable({
  providedIn: 'root',
})
export class GeminiService {
  #generativeAI = new GoogleGenerativeAI(environment.geminy_key);
 
  #prompt =
    'You are an expert in the Kendo UI library for Angular, provide a real-world scenario and how this product ' +
    'helps to solve, with angular code examples and links for resources';
 
  #model = this.#generativeAI.getGenerativeModel({
    model: 'gemini-pro',
  });
  
}

Ahora toca generar el contenido con el modelo. Primero, creamos un método llamado generate que recibe el parámetro textInput. Como el método generateContent es asíncrono, la firma de generate también debe ser async.

Primero definimos parts para preparar el contenido que vamos a generar, combinando el prompt base y el textInput que envía el usuario. Esto le da contexto al modelo. Luego obtenemos modelResult, que devuelve varias propiedades. En nuestro caso, la más importante es response. Este objeto incluye la función text() para extraer el texto plano de la respuesta. Para probar que todo funciona, podemos imprimirlo en la consola usando console.log().

Usamos try/catch para manejar cualquier error durante la petición a generateContent.

 async generate(textInput: string) {
    try {
      if (text) {
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput }
        ];
 
        const modelResult = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });
        const response = modelResult.response;
        const text = response.text();
         console.log(text)
      }
    } catch (e: any) {
      console.log(e);
    }
  }

Para usar el servicio, seguimos estos pasos: inyectamos GeminiService en AppComponent.ts, agregamos un método llamado getAnswer y llamamos al método generate del servicio.

export class AppComponent {
  geminiService = inject(GeminiService)
 
  async getAnswer(text: any) {
    await this.geminiService.generate(text);
  }
 
}

En la plantilla HTML, agregamos un input con una referencia de plantilla #userInput. En el button, llamamos al método getAnswer.

<input #userInput><button (click)="getAnswer(userInput.value)">Generate</button>

Guardamos los cambios y ¡aquí tenemos nuestra "IA de Kendo con Gemini"! 🎉😎

Bien, funciona, pero visualmente no se ve muy bien. Vamos a darle un diseño profesional y atractivo usando Kendo Conversational UI.

Usando Conversational UI

Para comenzar, instalamos Kendo UI Conversational UI para Angular usando schematics:

ng add @progress/kendo-angular-conversational-ui

Importamos ChatModule en la sección de imports de app.component.ts, ya que este módulo provee el componente kendo-chat.

 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule,   ChatModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {

Ahora viene la parte interesante en gemini.service.ts. Aquí vamos a usar Signals para sincronizar la respuesta de Gemini con el componente kendo-chat, el usuario y los mensajes.

Necesitamos declarar dos usuarios para el chat: uno será kendoIA y el otro será el user (en mi caso, yo). Cada usuario debe tener un ID único. Para mostrar avatares en el chat, podemos usar imágenes locales desde la carpeta assets.

Estoy usando crypto.randomUUID() para generar un ID único.

  readonly #kendoIA: User = {
    id: crypto.randomUUID(),
    name: 'Kendo UI',
    avatarUrl: './assets/kendo.png',
  };
 
  public readonly user: User = {
    id: crypto.randomUUID(),
    name: 'Dany',
    avatarUrl: './assets/dany.jpg',
  };

Declaramos una señal $messages con un arreglo de tipo Message y la inicializamos con un mensaje de bienvenida:

  $messages = signal<Message[]>([{
          author: this.#kendoIA,
          timestamp: new Date(),
          text: 'Hi! 👋 how I can help you with Kendo ?'
  }]);

El componente kendo-chat emite un evento SendMessageEvent, el cual contiene toda la información del mensaje desde la interfaz: el usuario, el texto y otros datos. Actualizamos el método generate para recibir este tipo SendMessageEvent y ajustamos la lógica inicial para leer textInput.message.text.

async generate(textInput: SendMessageEvent) {
    try {
      if (textInput.message.text && this.#model) {

Después, actualizamos la señal $messages usando el método update, agregando el nuevo mensaje enviado y configurando el contenido para el modelo a partir de textInput.message.text.

     this.$messages.update((p) => [...p, textInput.message]);
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput.message.text },
        ];

Creamos un nuevo mensaje con la respuesta que devuelve el modelo, le asignamos el autor author: this.#kendoIA y actualizamos nuevamente la lista de mensajes en la señal.

 const message = {
          author: this.#kendoIA,
          timestamp: new Date(),
          text,
        };
 
        this.$messages.update((p) => [...p, message]);

El código completo de este método queda así:

async generate(textInput: SendMessageEvent) {
    try {
      if (textInput.message.text && this.#model) {
        this.$messages.update((p) => [...p, textInput.message]);
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput.message.text },
        ];
 
        const result = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });
        
        const response = result.response;
        const text = response.text();
 
        const message = {
          author: this.#kendoIA,
          timestamp: new Date(),
          text,
        };
 
        this.$messages.update((p) => [...p, message]);
      }
    } catch (e: any) {
      console.log(e);
    }
  }

Conectando Kendo Chat con Signals

¡Esta es la parte divertida de conectar todo con Signals! Vamos a tomar $messages y user directamente del servicio de Gemini. Primero declaramos las propiedades en app.component.ts.

export class AppComponent {
 
  geminiService = inject(GeminiService)
  user = this.geminiService.user;
  messages = this.geminiService.$messages
 
  async generate(event: SendMessageEvent) {
    await this.geminiService.generate(event);
  }
}

En la plantilla HTML, agregamos <kendo-chat> y enlazamos la propiedad user. Luego leemos la señal messages() (con paréntesis al ser una señal) y vinculamos el evento sendMessage al método generate que definimos antes.

Como queremos mostrar tanto el avatar como el texto, personalizamos la plantilla por defecto usando la directiva kendoChatMessageTemplate para tener acceso a la variable del mensaje.

El marcado final queda de la siguiente forma:

  <kendo-chat [user]="user" [messages]="messages()" (sendMessage)="generate($event)" width="450px">
    <ng-template kendoChatMessageTemplate let-message>
      <div>
      {{ message.text }}
      {{message.avatar}}
      </div>
  </ng-template>

Guardamos los cambios y ¡listo! Ya tenemos a Gemini funcionando con una interfaz de chat moderna y fluida.

Todo funciona, pero luego de mostrarle a @Jörgen de Groot mi demo del primer chat usando Gemini and the Conversational UI, me preguntó cómo mantener el historial y el contexto sin necesidad de enviar el prompt en cada interacción.

Contexto y consumo de tokens

El primer enfoque de enviar el prompt una y otra vez genera un gasto innecesario de tokens, además de que no mantiene el contexto inicial ni conserva el historial de la conversación.

Gemini ofrece una funcionalidad de chat que almacena las preguntas y respuestas, lo que permite generar respuestas interactivas e incrementales dentro de un mismo contexto. Esto es justo lo que necesitamos para nuestro chatbot con Kendo, así que vamos a implementarlo.

La sesión de chat (ChatSession)

En nuestro primer intento usamos directamente el model de Gemini para generar contenido. Esta vez vamos a recurrir al método startChat del modelo para obtener un objeto ChatSession, el cual conserva el historial y el contexto inicial junto con el prompt.

El modelo de Gemini permite iniciar una sesión con startChat, donde definimos nuestro prompt inicial y el punto de partida de la conversación. El objeto ChatSession cuenta con el método sendMessage, lo que nos permite enviar únicamente el mensaje nuevo del usuario en cada turno.

Primero declaramos la propiedad #chatSession con el historial inicial, donde incluimos el prompt base y una primera respuesta simulada del modelo:

 #chatSession = this.#model.startChat({
    history: [
      {
        role: 'user',
        parts: this.#prompt,
      },
      {
        role: 'model',
        parts: "Yes, I'm a Angular expert with Kendo UI",
      },
    ],
    generationConfig: {
      maxOutputTokens: 100,
    },
  });

El siguiente paso es usar chatSession en lugar de enviar manualmente las partes y el rol user al modelo cada vez:

     const result = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });

Reemplazamos la llamada al modelo por chatSession y usamos su método sendMessage:

 const result = await this.#chatSession.sendMessage(
          textInput.message.text,
       );

¡Listo! 🎉 Ahora nuestro chatbot cuenta con soporte para historial y conversación continua sin tener que reenviar el prompt completo en cada mensaje, lo que nos ahorra una buena cantidad de tokens 😊😁

Mira la demo en acción: 👇

Resumen

En este artículo vimos cómo combinar Gemini con Angular e integrarlo con Angular Conversational UI para crear una interfaz de usuario atractiva y fácil de usar.

También agregamos soporte de historial a nuestro chat, lo que mejora la experiencia del usuario y optimiza el consumo de tokens. Mantener el historial y el contexto en Gemini evita tener que reenviar las instrucciones iniciales una y otra vez.

Aprovechar la función de chat mediante ChatSession nos permite gestionar respuestas incrementales con el contexto adecuado, ofreciendo una experiencia mucho más natural y ahorrando tokens en el proceso. 💰🎉

Parte de la serie Frontend

Modern frontend development — Angular, React, TypeScript, accessibility, and performance.

Ver serie completa

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.