La UI Generativa es el Nuevo Frontend. La Lanzamos Hace Meses.
Todo el mundo habla de UI Generativa ahora mismo. CopilotKit publicó una guía para desarrolladores. Google tiene un blog de investigación sobre ello.
La lanzamos en enero. Ha estado funcionando en producción dentro de Elba desde entonces.
Esta no es una opinión apresurada. Es un análisis de cómo lo construimos realmente, cómo es la arquitectura y qué nos hizo tropezar en el camino.
¿Qué significa realmente la UI Generativa?
La versión corta: en lugar de codificar cada estado de la pantalla, el agente de IA decide en tiempo de ejecución qué componente de UI mostrar y con qué datos rellenarlo.
La interfaz de chat ya no devuelve solo texto. Devuelve UI interactivas y en vivo, renderizadas dentro de la propia conversación.
CopilotKit divide esto en tres patrones:
- UI Generativa Estática: el frontend posee los componentes. El agente elige cuál mostrar y lo rellena con datos. Alto control, predecible.
- UI Generativa Declarativa: el agente devuelve una especificación de UI (tarjetas, formularios, listas en JSON), y el frontend renderiza a partir de esa especificación. Más flexible, más impredecible.
- UI Generativa Abierta: el agente devuelve una superficie de UI completa, a menudo un iframe o HTML arbitrario. Máxima flexibilidad, mínima consistencia.
Elegimos el primer patrón. Pre-construimos cada componente. El agente nunca toca el diseño. Solo decide cuándo aparece algo y qué va dentro.
Esa fue la decisión correcta para un asistente de soporte en un producto de producción. Quieres que el agente tome decisiones, no que diseñe interfaces.
Cómo funciona Frida
Frida es el asistente de IA dentro del producto Elba. Se ejecuta dentro de un panel de chat. Los usuarios pueden preguntarle sobre su cuenta, llamadas recientes, saldo de crédito, facturación y cualquier otra cosa sobre la plataforma.
Lo que lo hace UI Generativa: cuando preguntas "¿cuál es mi saldo de crédito?", Frida no responde con texto. Renderiza una tarjeta.
Cuando preguntas por llamadas recientes, obtienes un registro de llamadas interactivo. Cuando estás en la ruta Empresarial, obtienes un formulario de cualificación clickeable, no una indicación de texto pidiéndote que escribas tu caso de uso.
La interfaz se adapta a lo que el agente necesita que el usuario haga.
La pila tecnológica
- Frontend: React, CopilotKit v2, protocolo AG-UI
- Backend: Agente PydanticAI ejecutándose en nuestro servicio Cognition Hub
- Protocolo: Hook
useFrontendTool de CopilotKit sobre AG-UI
El registro de componentes
Cada componente generativo en Frida está registrado en un solo lugar:
// toolComponentRegistry.ts
export const TOOL_COMPONENTS: Record<string, React.FC<ToolComponentProps>> = {};
export function registerToolComponent(
name: string,
component: React.FC<ToolComponentProps>
): void {
TOOL_COMPONENTS[name] = component;
}
Al inicio, cada componente se registra a sí mismo:
registerToolComponent('showCreditBalance', CreditBalanceCard);
registerToolComponent('showRecentCalls', RecentCallsCard);
registerToolComponent('showCreditBurnRates', CreditBurnRateCard);
registerToolComponent('showSalesQualifier', SalesQualifierInChat);
registerToolComponent('showSalesInquiryPreview', SalesInquiryPreview);
Cuando el agente dispara una llamada a una herramienta, el renderizador de chat la busca en este registro y la monta en línea.
Las definiciones de herramientas
Cada componente tiene una definición useFrontendTool correspondiente. Aquí es donde el agente aprende cuándo y cómo usarla:
useFrontendTool(
{
name: 'showCreditBalance',
description:
'SIEMPRE usa esta herramienta para renderizar una tarjeta visual de saldo de crédito cuando el usuario pregunte sobre sus créditos, saldo, minutos restantes o estado de facturación.',
parameters: creditBalanceSchema,
handler: async (params) => JSON.stringify(params),
},
[],
);
Los parámetros se validan con Zod:
const creditBalanceSchema = z.object({
credits: z.number().describe('Saldo de crédito actual'),
plan: z.string().describe('Nombre del plan actual'),
status: z.enum(['healthy', 'low', 'critical']).describe(
'Estado del saldo: saludable (>100), bajo (10-100), crítico (<10)'
),
estimatedMinutes: z.number().describe('Minutos de voz estimados restantes'),
});
El agente lee el esquema. Las descripciones del esquema lo entrenan sobre qué valores pasar. estimatedMinutes con su descripción (credits / 10) significa que el agente calcula el valor correcto en lugar de adivinar.
El agente backend
La otra mitad de esto vive en el backend de Python. Frida es un agente PydanticAI con herramientas que puede llamar para obtener datos reales:
@agent.tool
async def fetch_recent_calls(ctx: RunContext[FridaSupportDeps], limit: int = 5) -> dict:
"""Obtener llamadas recientes para la organización del usuario."""
calls = await _get_recent_calls(client, ctx.deps.org_id, limit=limit)
formatted = [_format_call_record(c) for c in calls]
return tool_result({"calls": formatted, "total": len(formatted)})
El agente tiene dos tipos de herramientas: herramientas de backend que obtienen datos (llamadas, información de la organización, base de conocimientos) y herramientas de frontend que renderizan UI. Las herramientas de backend alimentan de datos a las herramientas de frontend. Ese es el ciclo completo.
Cuando un usuario pregunta "¿cuáles son mis llamadas recientes?", el agente:
- Llama a
fetch_recent_calls para obtener los datos de la API
- Llama a
showRecentCalls con los resultados formateados
- El frontend recibe la llamada a la herramienta, busca
RecentCallsCard en el registro y la monta
Sin texto. Sin análisis. Solo un componente renderizado con datos reales.
Inyección de contexto
El agente sabe más que solo lo que el usuario escribió. Inyectamos contexto en tiempo de ejecución a través de generadores de instrucciones dinámicos:
def page_context(ctx: RunContext[FridaSupportDeps]) -> str:
"""Añadir contexto de la página actual."""
page = ctx.deps.current_page
for prefix, hint in _PAGE_HINTS.items():
if page.startswith(prefix):
return f"Contexto de página actual: {hint}"
return f"El usuario está actualmente en la página: {page}"
El agente sabe en qué página está el usuario, su estado de facturación, saldo de crédito, plan, número de agentes y la fecha de hoy. Ese contexto da forma a cada respuesta. "Revisa mi saldo de crédito" de alguien en la página /billing se trata de manera diferente que de alguien en /agents.
La inyección de fecha merece una mención especial:
def date_context(ctx: RunContext[FridaSupportDeps]) -> str:
parts.append(
f"IMPORTANTE — La fecha de hoy es {ctx.deps.current_date}. "
"Úsala como fuente de verdad para 'hoy', 'ayer', 'esta semana', etc. "
"NO infieras la fecha actual de las marcas de tiempo de las llamadas ni de ningún otro dato."
)
Sin esto, el modelo intentará inferir la fecha actual de la marca de tiempo de la llamada más reciente, lo cual es incorrecto. Mostrará una llamada de hace tres días y dirá "Hoy". Lo detectamos en las pruebas. La inyección explícita de fecha con la instrucción "NO infieras" lo solucionó.
Las partes de las que nadie habla
El camino feliz es fácil. Esto es lo que realmente llevó tiempo.
Estados esqueleto durante el streaming
CopilotKit transmite los argumentos de las llamadas a herramientas. El componente se monta antes de que lleguen todos los argumentos. Si renderizas inmediatamente con datos parciales, obtienes estados de carga parcial molesto.
Añadimos un retraso de 500 ms antes de mostrar el componente real, con un brillo esqueleto en el hueco:
const DelayedToolComponent: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [ready, setReady] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setReady(true), 500);
return () => clearTimeout(timer);
}, []);
if (!ready) return <ToolComponentSkeleton />;
return <>{children}</>;
};
El esqueleto coincide con la forma del componente real. La tarjeta de saldo de crédito obtiene un esqueleto que parece una tarjeta de saldo de crédito. Esto hizo que la experiencia pareciera intencional en lugar de rota.
Límites de error por componente
Cuando un componente falla, no quieres que todo el chat se rompa. Envolvemos cada renderizado de componente de herramienta en un límite de error:
class ToolComponentErrorBoundary extends Component<...> {
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) {
return (
<div className="...amber border...">
<p>No se puede mostrar este contenido; inténtalo de nuevo.</p>
</div>
);
}
return this.props.children;
}
}
Esto le da al usuario un error recuperable en lugar de una sesión de chat rota. El fallback ámbar es lo suficientemente ligero como para no parecer alarmante, pero lo suficientemente claro como para que el usuario sepa que algo no se renderizó.
Componentes interactivos que activan el siguiente turno del agente
La tarjeta de cualificación de ventas es interactiva. El usuario hace clic en las opciones, presiona enviar, y sus selecciones deben convertirse en el siguiente mensaje de la conversación. Hacemos esto a través de un evento DOM personalizado:
const SalesQualifierInChat: React.FC = () => {
const handleSubmit = (data: QualifierState) => {
const message = `Caso de uso: ${data.useCase}. Volumen: ${data.volume}. Cronograma: ${data.timeline}.`;
window.dispatchEvent(
new CustomEvent('frida:qualifier-submit', { detail: message })
);
};
return <SalesQualifierCard onSubmit={handleSubmit} />;
};
El chat escucha este evento e inyecta la cadena formateada como un mensaje de usuario. Luego, el agente lo recoge y continúa el flujo de trabajo, redactando el correo electrónico y mostrando la tarjeta de vista previa. Dos componentes de herramienta, un flujo de conversación.
Por qué la UI Generativa Estática fue la decisión correcta
Podríamos haber optado por lo declarativo. Podríamos haber dejado que el agente devolviera JSON A2UI y renderizado a partir de la especificación. Eso le habría dado a Frida la capacidad de inventar nuevos diseños.
No queríamos eso. Frida vive dentro de un producto de producción con un sistema de diseño. Un agente que puede inventar tarjetas arbitrarias también es un agente que puede producir UI fuera de marca o rota a las 2 a.m. cuando un cliente está bloqueado.
La UI Generativa Estática te da la capacidad de respuesta de una interfaz de IA con la previsibilidad de una biblioteca de componentes hecha a mano. El agente toma decisiones. El frontend crea píxeles. Ninguno de los dos se entromete en el trabajo del otro.
La restricción también es lo que la hizo publicable. Frida se lanzó con seis componentes. Cada uno es un contrato claramente definido entre el agente y la UI. Cuando el agente llama a showCreditBalance, sabemos exactamente qué se renderiza. Cuando no lo hace, no se renderiza nada. Sin sorpresas.
La UI Generativa no es un concepto nuevo. El nombre es nuevo. La idea de que un asistente de IA debe renderizar UI apropiada para la tarea en lugar de narrarlo todo como texto, eso ha sido obvio durante un tiempo.
Si estás construyendo un asistente de IA dentro de un producto y solo devuelve texto, eso es lo siguiente a lo que debes prestar atención. Elige un componente, una llamada a herramienta, un caso de uso. El patrón es lo suficientemente pequeño como para lanzarlo en un día. La diferencia en la experiencia no es pequeña en absoluto.
#Elba #BuildingVoiceAI