Fecha: 2025-01-22 Prioridad: 🔴 CRÍTICA Estado: ✅ DIAGNOSTICADO
Problema reportado: Usuario envía mensajes pero no recibe respuestas de la IA.
Comportamiento observado:
- ✅ Los mensajes del usuario se envían correctamente
- ✅ El API responde con 200 OK
- ✅ El API tarda 4-24 segundos (normal para OpenAI)
- ❌ Las respuestas NO aparecen en la UI
- ❌ No hay errores visibles en el console
Respuesta: El código del frontend está parseando el stream incorrectamente.
Evidencia:
// src/app/chat/page.tsx:105
for (const line of lines) {
if (line.startsWith("0:")) { // ❌ NUNCA se cumple esta condición
const text = line.slice(2).replace(/^"|"$/g, "")
assistantMessage += text
// ...
}
}El código busca líneas que empiezan con "0:", pero el stream no viene en ese formato.
Respuesta: El API usa streamText().toTextStreamResponse() que retorna texto plano, no el formato "0:".
Evidencia:
// src/app/api/chat/route.ts:86-102
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [...],
temperature: 0.7,
})
// ❌ toTextStreamResponse() retorna plain text stream
const response = result.toTextStreamResponse()
return responseFormato del stream:
// ❌ Frontend espera:
0:"Hola"
0:" como"
0:" estas"
// ✅ API envía (Vercel AI SDK v3):
Hola
como
estas
Respuesta: Incompatibilidad entre versiones de Vercel AI SDK.
Historia:
- Vercel AI SDK v2: Usaba React Server Components (RSC) stream con formato
"0:text" - Vercel AI SDK v3+: Usa
toTextStreamResponse()que retorna plain text stream - Problema: El frontend se quedó con el parser antiguo (v2) pero el API usa el nuevo (v3)
Documentación:
// AI SDK v3 - Texto plano
const response = result.toTextStreamResponse()
// Stream: "Hello world..."
// AI SDK v2 - RSC format
const response = result.toDataStreamResponse()
// Stream: "0:\"Hello\"\n0:\" world\"\n"Respuesta: El código funcionaba con conversaciones nuevas pero falló al cargar conversaciones viejas.
Explicación:
- Durante desarrollo inicial, se probó creando conversaciones nuevas
- Esas conversaciones se guardaban con respuestas vacías o el dev no notó que faltaban
- Al arreglar el bug de timestamps, el usuario empezó a cargar conversaciones viejas
- Las conversaciones viejas mostraban solo mensajes del usuario (sin respuestas de IA)
- El problema se hizo evidente
Timeline:
1. Desarrollo inicial → Chat implementado con parser incorrecto
2. Testing básico → Solo se probaron 1-2 mensajes, parecía funcionar
3. Bug de timestamps → Se arregló la carga de conversaciones
4. Usuario prueba → Carga conversación vieja, nota que no hay respuestas
5. Usuario envía nuevo mensaje → Tampoco recibe respuesta
6. 🔴 BUG REPORTADO
🎯 ROOT CAUSE: Incompatibilidad de formato de streaming entre API y Frontend
Cadena de causas:
┌──────────────────────────────────────────────────────────┐
│ API usa Vercel AI SDK v3 (toTextStreamResponse) │
│ ↓ │
│ Stream formato: plain text chunks │
│ ↓ │
│ Frontend parser espera formato RSC ("0:") │
│ ↓ │
│ Condición if (line.startsWith("0:")) NUNCA se cumple │
│ ↓ │
│ assistantMessage nunca se actualiza │
│ ↓ │
│ UI no muestra respuestas ❌ │
└──────────────────────────────────────────────────────────┘
Factores contribuyentes:
- ❌ Falta de tests E2E - No se probó el flujo completo de chat
- ❌ Falta de tests de integración - No se verificó que el streaming funcionara
- ❌ Falta de logging en frontend - No se logueaba el contenido del stream
- ❌ Testing superficial - Solo se probó que el API respondía 200, no que mostrara texto
- ❌ No se revisó la documentación - Vercel AI SDK cambió el formato en v3
POST /api/chat 200 in 15.2s ✅
POST /api/chat 200 in 8.9s ✅
POST /api/chat 200 in 24.2s ✅
POST /api/chat 200 in 4.4s ✅
// Stream llega con chunks como:
"Hola, soy PlayGPT EDU..."
// Código busca:
if (line.startsWith("0:")) { // ❌ NUNCA es true
// Este código NUNCA se ejecuta
}
// Resultado:
assistantMessage === "" // ❌ Siempre vacío
setMessages() no se llama // ❌ UI no se actualizaHay 2 opciones:
Ventajas:
- ✅ Más simple y directo
- ✅ Mejor performance (menos parsing)
- ✅ Siguiendo el estándar de AI SDK v3
- ✅ No requiere cambios en API
Desventajas:
⚠️ Requiere cambiar el parser del frontend
Implementación:
// ANTES (❌ incorrecto)
for (const line of lines) {
if (line.startsWith("0:")) {
const text = line.slice(2).replace(/^"|"$/g, "")
assistantMessage += text
}
}
// DESPUÉS (✅ correcto)
const chunk = decoder.decode(value, { stream: true })
assistantMessage += chunk
// Update UI
setMessages((prev) => {
const existing = prev.find((m) => m.id === assistantMessageId)
if (existing) {
return prev.map((m) =>
m.id === assistantMessageId
? { ...m, content: assistantMessage }
: m
)
} else {
return [
...prev,
{
id: assistantMessageId,
role: "assistant",
content: assistantMessage,
timestamp: new Date(),
},
]
}
})Ventajas:
- ✅ Soporte para metadata adicional
- ✅ Soporte para tool calls en futuro
Desventajas:
⚠️ Más complejo⚠️ Requiere cambios en ambos lados
Justificación:
- Más rápido de implementar
- Más simple de mantener
- Suficiente para el caso de uso actual
- Sigue el patrón estándar de AI SDK v3
// Read the stream
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error("No reader available")
}
let assistantMessage = ""
const assistantMessageId = (Date.now() + 1).toString()
// ✅ NUEVO: Agregar el mensaje vacío primero
setMessages((prev) => [
...prev,
{
id: assistantMessageId,
role: "assistant",
content: "",
timestamp: new Date(),
},
])
while (true) {
const { done, value } = await reader.read()
if (done) break
// ✅ NUEVO: Decodificar el chunk directamente
const chunk = decoder.decode(value, { stream: true })
assistantMessage += chunk
// ✅ NUEVO: Actualizar el mensaje en cada chunk
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMessageId
? { ...m, content: assistantMessage }
: m
)
)
}Cambios clave:
- ✅ Eliminar parsing de líneas "0:"
- ✅ Decodificar directamente el chunk
- ✅ Agregar mensaje vacío al inicio
- ✅ Actualizar en cada chunk (no solo cuando encuentra "0:")
-
Enviar mensaje simple:
- Input: "hola"
- Verificar: Respuesta aparece letra por letra
- Verificar: Mensaje completo se muestra
-
Enviar pregunta compleja:
- Input: "¿qué es la falacia del jugador?"
- Verificar: Respuesta se genera correctamente
- Verificar: Formato markdown se renderiza
-
Múltiples mensajes:
- Enviar 3-4 mensajes seguidos
- Verificar: Cada uno recibe respuesta
- Verificar: Historial completo se mantiene
-
Cargar conversación vieja:
- Abrir conversación existente
- Enviar nuevo mensaje
- Verificar: Respuesta aparece correctamente
✅ No debe haber: "Error sending message"
✅ No debe haber: "No reader available"
✅ Debe haber: Respuestas visibles en UI
✅ Debe haber: Streaming en tiempo real (letra por letra)
| Métrica | Antes | Después (Esperado) |
|---|---|---|
| Respuestas aparecen | ❌ 0% | ✅ 100% |
| Streaming funciona | ❌ Roto | ✅ Tiempo real |
| Console errors | ✅ 0 (pero no funciona) | ✅ 0 |
| User experience | 💔 Roto | ✅ Funcional |
| Latencia percibida | ⏳ Alta (no muestra nada) | ⚡ Baja (streaming) |
// tests/e2e/chat.spec.ts
test('chat responds to user message', async ({ page }) => {
await page.goto('/chat')
await page.fill('[placeholder*="Pregúntame"]', 'hola')
await page.press('[placeholder*="Pregúntame"]', 'Enter')
// Esperar a que aparezca respuesta
await expect(page.locator('text=/PlayGPT EDU/i')).toBeVisible()
await expect(page.locator('text=/hola/i')).toBeVisible()
})// Debug logging en desarrollo
if (process.env.NODE_ENV === 'development') {
console.log('📥 Chunk received:', chunk)
console.log('📝 Current message:', assistantMessage)
}<ErrorBoundary fallback={<ChatError />}>
<ChatContainer messages={messages} />
</ErrorBoundary>- Mostrar "..." mientras escribe
- Mostrar progress bar durante streaming
- Mostrar cuando está buscando en knowledge base
❌ Asumió formato:
if (line.startsWith("0:")) // Asunción sin verificar✅ Verificar con logs:
console.log('Stream chunk:', chunk) // Ver formato realEl cambio de formato estaba documentado en:
- Vercel AI SDK Migration Guide (v2 → v3)
- Pero no se revisó
Un test E2E simple habría detectado esto:
test('user receives response after sending message')Sin logs en el frontend, fue difícil diagnosticar:
// Agregar en todos los parsers
console.log('Stream format:', chunk)Por qué es crítico:
- ❌ Funcionalidad principal completamente rota
- ❌ No hay workaround para el usuario
- ❌ Hace la app inutilizable
- ❌ Afecta al 100% de los usuarios
- ✅ Todos los usuarios nuevos
- ✅ Todos los usuarios existentes
- ✅ 100% de conversaciones
- Desde la implementación inicial
- No detectado en testing
- Detectado por el usuario en producción
Estado: ✅ Diagnosticado, listo para implementar Prioridad: 🔴 CRÍTICA Esfuerzo estimado: 10 minutos Risk: Bajo (cambio aislado)