GenAINextVercel AI SDKLangchainGoogle Gemini

Construyendo Interfaces Conversacionales. Parte 2

Antonio Pérez
2025-08-17
7
Construyendo Interfaces Conversacionales. Parte 2

Refactorizando nuestro proyecto

Podrás encontrar el código de este artículo en: https://github.com/aperezl/ai-fullstack-serie/tree/chatbot-part1

En la primera parte, nos sumergimos en la mecánica del diálogo en tiempo real con LLMs, diseccionando el useChat y el protocolo de streaming que lo sustenta. Logramos una prueba de concepto funcional, pero incompleta. Nuestra aplicación, en su estado actual, es un único fichero page.tsx que no puede crecer.

Antes de añadir más complejidad a nuestra lógica de chat,tenemos que dar un paso atrás. El código que funciona no siempre es el código correcto. Este capítulo está dedicado a la refactorización: transformaremos nuestra aplicación de una sola página en una estructura modular y navegable. Este no es un simple ejercicio de limpieza; es la construcción de un andamiaje robusto que nos permitirá añadir nuevas secciones y funcionalidades complejas sin incurrir en deuda técnica.

Abordaremos los siguientes pasos, explicando el "porqué" de cada decisión:

  • Estructura de Componentes: Crearemos una organización de ficheros lógica para nuestros componentes de UI.
  • Layout Persistente: Implementaremos una barra de navegación lateral que se mantenga constante a través de las diferentes secciones de la aplicación.
  • Gestión de Rutas: Reorganizaremos nuestras páginas para reflejar una estructura de navegación coherente, sentando las bases para futuras expansiones.
  • Página de Inicio (Hub): Crearemos un punto de entrada claro que sirva como un índice para las distintas funcionalidades de la aplicación.

Este proceso es el equivalente a cimentar el terreno antes de construir un rascacielos. Es un trabajo fundamental que garantiza la estabilidad y mantenibilidad del proyecto a largo plazo.

Nuestros primeros componentes

Una aplicación que crece necesita una distinción clara entre las "páginas" (vistas de ruta) y los "componentes" (bloques de UI reutilizables). El primer paso lógico es crear un directorio /components en la raíz de /src.

Nuestro primer componente será la pieza central de nuestra nueva arquitectura de layout: la barra de navegación lateral o Sidebar. Para los iconos, nos apoyaremos en lucide-react, una excelente librería de iconos SVG, ligera y fácilmente "tree-shakable" (solo se incluye en el bundle final el código de los iconos que realmente usamos).

bash
npm i lucide-react

Ahora, creemos el fichero /src/components/Sidebar.tsx:

tsxsrc/components/Sidebar.tsx
"use client"

import Link from "next/link"
import { usePathname } from "next/navigation"
import { Home, BookOpen, MessageCircle } from "lucide-react"

const navigation = [
  { name: "Home", href: "/", icon: Home },
  { name: "Fundamental", href: "/fundamental", icon: BookOpen },
  { name: "Chatbot", href: "/chatbot", icon: MessageCircle },
]

export function Sidebar() {
  const pathname = usePathname()

  return (
    <div className="w-64 bg-slate-700 text-white flex flex-col">
      <div className="p-6">
        <h1 className="text-xl font-bold text-yellow-500">ai-fullstack-serie</h1>
      </div>

      <nav className="flex-1 px-4 space-y-2">
        {navigation.map((item) => {
          const isActive = pathname === item.href
          return (
            <Link
              key={item.name}
              href={item.href}
              className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${isActive ? "bg-yellow-500 text-slate-900" : "text-slate-300 hover:bg-slate-600 hover:text-white"
                }`}
            >
              <item.icon className="mr-3 h-5 w-5" />
              {item.name}
            </Link>
          )
        })}
      </nav>
    </div>
  )
}

Sidebar

Análisis del componente:

  • "use client": Esta directiva es crucial. Marca este componente como un "Client Component". Es un requisito indispensable porque estamos utilizando el hook usePathname, que necesita acceder al estado del navegador (la URL actual) y no puede ejecutarse en el servidor.
  • usePathname(): Este hook de Next.js nos devuelve la ruta actual de la URL. Es la herramienta que nos permite saber qué enlace de la navegación debe aparecer como activo, una pieza fundamental para la UX.
  • Estructura de Datos nav: Definir los enlaces en un array de objetos es una práctica limpia. Permite iterar y renderizar los elementos de navegación de forma dinámica, facilitando la adición o eliminación de enlaces en el futuro.
  • Estilado Condicional: La lógica isActive ? '...' : '...' dentro del className es un patrón estándar en Tailwind CSS para aplicar estilos dinámicamente. En este caso, resalta el enlace activo, proporcionando feedback visual inmediato al usuario.

Integración en un Layout persistente

Con nuestro componente Sidebar listo, necesitamos un lugar donde pueda vivir de forma permanente, sin tener que ser re-renderizado en cada cambio de página. El fichero /src/app/layout.tsx es el lugar designado por Next.js para esta tarea. Actúa como la plantilla raíz que envuelve a toda la aplicación.

Modificamos el RootLayout para adoptar una estructura de dos columnas:

tsxsrc/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Sidebar } from "@/components/Sidebar"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <div className="flex h-screen bg-gray-50">
          <Sidebar />
          <main className="flex-1 overflow-auto bg-slate-800">{children}</main>
        </div>
      </body>
    </html>
  );
}

Análisis del Layout:

  • Estructura flex: El div contenedor principal usa flex y h-screen para establecer un layout que ocupa toda la altura de la ventana.
  • Componente Sidebar Estático: Colocamos directamente dentro de este div. Al estar fuera del componente {children}, Next.js entiende que es parte del layout persistente. No se desmontará ni se volverá a montar durante las navegaciones entre páginas, conservando su estado.
  • Contenido Dinámico {children}: La prop children es donde Next.js renderizará el contenido de la página activa (por ejemplo, page.tsx). Al envolverlo en un tag
    con flex-1, nos aseguramos de que ocupe todo el espacio restante.

Reorganización de rutas

Nuestra lógica del generador de títulos virales inicial vivía en la raíz (/app/page.tsx). Con la nueva estructura, esta página se convertirá en la sección "Fundamental". El App Router de Next.js funciona basado en directorios, por lo que la reorganización es intuitiva:

  1. Movemos el contenido de /src/app/page.tsx a un nuevo fichero en /src/app/fundamental/page.tsx.

Este simple cambio de ubicación de ficheros actualiza la ruta de la aplicación. La lógica de chat ahora será accesible en http://localhost:3000/fundamental.

alt text

Creación de un nuevo punto de entrada

La ruta raíz (/) ahora está vacía. La convertiremos en un "hub" o página de inicio que dirija al usuario a las diferentes secciones de nuestra aplicación en crecimiento.

Creamos el nuevo fichero /src/app/page.tsx:

tsxsrc/app/page.tsx
import Link from "next/link"
import { BookOpen, MessageCircle, LucideIcon } from "lucide-react"

type Card = {
  title: string
  Icon: LucideIcon
  path: string
}

const items: Card[] = [
  {
    title: 'Fundamentos modernos y primera interacción con IA.',
    Icon: BookOpen,
    path: '/fundamental'
  },
  {
    title: 'Construyendo Interfaces Conversacionales (Chatbots)',
    Icon: MessageCircle,
    path: '/chatbot'
  }
]

const ItemComponent = ({ title, Icon, path }: Card) => (
  <Link href={path} className="block">
    <div className="flex items-center space-x-3 mb-4">
      <div className="p-2 bg-yellow-500/20 rounded-lg">
        <Icon className="h-6 w-6 text-yellow-400" />
      </div>
      <h3 className="text-xl font-semibold text-white">{title}</h3>
    </div>
  </Link>
)

export default function Home() {
  return (
    <div className="p-8 bg-slate-800 min-h-screen">
      <div className="flex flex-col mb-8 w-full items-center">
        <h1 className="text-3xl font-bold text-white mb-2">Serie: Aplicación FullStack</h1>
        <p className="text-slate-300">Explora las diferentes secciones de la aplicación</p>
      </div>
      <div className="flex flex-col p-6 w-full items-center">
        <div className="p-6">
          {items.map((item, index) => <ItemComponent key={index} {...item} />)}
        </div>
      </div>
    </div>
  )
}

alt text

Este componente es un Server Component por defecto, simple y eficiente. Su único propósito es presentar las secciones disponibles, actuando como un mapa del sitio funcional.

Conclusiones

No hemos escrito una sola línea nueva de lógica de IA en este capítulo, y sin embargo, el avance ha sido monumental. Hemos transformado un script aislado en el esqueleto de una aplicación real.

Mediante los componentes, la creación de un layout persistente y la organización de nuestras rutas, hemos establecido un patrón de desarrollo escalable. Ahora tenemos un sistema donde añadir una nueva sección (/chatbot, por ejemplo) es tan simple como crear una nueva carpeta y su correspondiente page.tsx, sabiendo que la navegación y la estructura general ya están resueltas.

Con esta fundación sólida en su sitio, estamos preparados para volver a la carga. En el próximo capítulo, construiremos la interfaz de usuario de nuestra sección /chatbot, aprovechando la robusta arquitectura que hemos establecido hoy.

Puedes encontrar el código de esta sección en: https://github.com/aperezl/ai-fullstack-serie/tree/chatbot-part1