MEMORIA FUNCIONAL — CaptionAI
Producto: CaptionAI — Generador de captions con IA
URL live: https://captionai.theboomer.dev
Stack: React + Vite (frontend SPA), FastAPI (Python) /api/v1, MongoDB, Clerk (auth), Stripe (billing)
Fuente de verdad: code/frontend/src/App.tsx (670 líneas, SPA de una sola pantalla con vistas internas), components/, pages/, services/, hooks/
Fecha de documentación: 2026-08-05
1. Introducción
Propósito
CaptionAI es un generador de captions (textos para redes sociales) impulsado por IA. El usuario introduce un tema, elige plataforma, tono, tipo de contenido e idioma de salida, y el backend (FastAPI + LLM con cadena de proveedores OpenRouter → DeepSeek → Gemini → GPT-4o) devuelve un caption estructurado con: hook (gancho), cuerpo, CTA, emojis y hashtags sugeridos.
Público objetivo
- Creadores de contenido y social media managers que publican en Instagram, X/Twitter, LinkedIn, TikTok y Facebook.
- Usuarios ocasionales que quieren un caption rápido sin registrarse (uso anónimo limitado).
- Equipos/agencias (plan Enterprise) que necesitan acceso programático vía API keys.
Nota de arquitectura
La app es una SPA de una sola página (App.tsx) con tres vistas internas manejadas por estado local (view: generator | billing | profile) y navegación por tabs en el header. No usa React Router. Todo el texto de la UI es bilingüe ES/EN mediante un objeto i18n inline.
2. Tipos de usuario (roles)
| Rol | Cómo se identifica | Capacidades |
|---|---|---|
| Anónimo (no autenticado) | isSignedIn === false (Clerk) |
Generar captions sin límite estricto de bloqueo (contador diario local anon_captions), ve el diálogo de upgrade tras su primer caption, no ve QuotaBadge. Sin acceso a billing/perfil completos (solo vista pública de planes y login prompt). |
| Usuario registrado (plan Free) | Sesión Clerk activa, sin suscripción Stripe | Quota diaria (3/día por defecto en QuotaBadge; 5/día en FREE_LIMITS de usePermissions), badge de cuota usados/límite, acceso completo a Facturación (ver planes, comprar créditos) y Perfil (info de usuario, plan y uso). |
| Suscriptor de pago (Basic/Pro/Enterprise) | Suscripción Stripe activa (summary.hasSubscription + status === 'active') |
Límites diarios según plan (limits.daily_captions), acceso API si limits.has_api, créditos extra consumibles. |
| Enterprise | planCode === 'enterprise' |
Todo lo anterior + gestión de API keys (generar, listar, revocar, IP whitelist) en la sección Perfil. |
3. Funcionalidades
F3.1 Generación de captions
Descripción: Función principal. POST a /api/v1/caption/generate con {tema, tipo_contenido, tono, plataforma, idioma}.
Flujo paso a paso:
1. El usuario rellena el campo Tema (textarea de 80px de alto, placeholder "De que quieres hablar?").
2. Selecciona Plataforma (pills): Instagram | X/Twitter | LinkedIn | TikTok | Facebook (por defecto instagram).
3. Selecciona Tono (pills, según idioma de UI): ES: profesional, casual, humor, inspirador, educativo, ventas | EN: professional, casual, humor, inspirational, educational, sales. Por defecto profesional/professional.
4. Selecciona Tipo de contenido (pills): ES: video, imagen, carousel, story, tweet | EN: video, image, carousel, story, tweet. Por defecto video.
5. Selecciona Idioma de salida (pills con bandera): ES, EN, FR, DE, ZH, JA, RU. Por defecto es.
6. Pulsa "Generar Caption" (botón primario full-width). El botón se deshabilita si loading o si el tema está vacío (disabled={loading || !tema.trim()}).
7. Si está autenticado, se añade header Authorization: Bearer <token Clerk> y se guarda el token en localStorage.clerk_token.
8. El backend responde con CaptionResult: {caption: {hook, cuerpo, emojis, hashtags, cta, completo}, plataforma}.
9. Tras éxito: si es anónimo, incrementa contador anon_captions (localStorage, por día) y abre el UpgradeDialog; si es usuario logueado, incrementa usage_log (localStorage) para el QuotaBadge.
Validaciones:
- Tema obligatorio: si tema.trim() está vacío, handleGenerate retorna sin llamar a la API (el botón además está deshabilitado).
- Error de API: se muestra errData.detail || 'Error <status>' en un banner rojo en el panel de resultado.
- Sin validación de longitud de tema en frontend (el backend aplica límites de caracteres por plan).
F3.2 Copiar caption
Descripción: Copia el caption completo al portapapeles.
Flujo: 1. Pulsar icono Copy (junto al título "Generated Caption"). 2. navigator.clipboard.writeText(result.caption.completo). 3. El icono cambia a check verde "Copiado!" durante 2 segundos.
F3.3 Regenerar caption
Descripción: Repite la generación con los mismos parámetros.
Flujo: 1. Pulsar icono RefreshCw en el panel de resultado. 2. Llama a handleGenerate de nuevo (mismos estados de formulario). 3. Si es anónimo y ya generó hoy, el UpgradeDialog se mostrará de nuevo.
F3.4 Autenticación con Clerk (Sign in / Sign up)
Descripción: Login/registro vía Clerk con modal.
Flujo:
1. Usuario anónimo: header muestra botón "Iniciar sesion" (SignInButton mode="modal").
2. Al hacer clic se abre el modal de Clerk (soporta Google OAuth y email/password según config de Clerk).
3. Tras autenticarse, SignedIn reemplaza el botón por: QuotaBadge + pill "Gratis" + UserButton (avatar).
4. afterSignOutUrl="/" — al cerrar sesión vuelve al inicio.
5. useClerkToken() sincroniza el token Clerk a localStorage.clerk_token para que los services de Stripe/API-keys lo usen.
F3.5 Selector de tema (dark/light/system)
Descripción: Cambia el tema visual de la app.
Flujo: 1. Tres botones con iconos (Moon/Sun/Monitor) en el header. 2. Persiste en localStorage.theme. 3. Si es system, escucha prefers-color-scheme y aplica el tema del SO en tiempo real. 4. Aplica/elimina la clase dark en document.documentElement.
F3.6 Selector de idioma UI (ES/EN)
Descripción: Cambia el idioma de toda la interfaz.
Flujo: 1. Select con opciones EN/ES en el header. 2. Persiste en localStorage.lang (por defecto es). 3. Re-renderiza todos los textos vía t(key). Nota: los valores de Tono/Tipo de contenido también cambian de idioma según el lang de UI.
F3.7 QuotaBadge — indicador de cuota diaria
Descripción: Badge en el header (solo usuarios logueados) que muestra usados/límite del día.
Flujo:
1. Al montar, si isSignedIn, llama en paralelo a GET /api/v1/billing/pricing-plans y GET /api/v1/billing/summary.
2. Si hay suscripción, busca el plan cuyo stripeProductId coincide y usa limits.daily_captions; si no, plan free con límite 3.
3. Muestra used/dailyLimit (p. ej. "2/3"). El used sale de localStorage.usage_log (contador diario).
4. Si el plan es free, el badge es clicable y navega a Facturación (title: "Ver planes").
5. Estados: carga → muestra "..." con cursor default; sin sesión → no renderiza nada.
F3.8 UpgradeDialog — prompt de registro para anónimos
Descripción: Modal que aparece al usuario anónimo tras generar su primer caption del día.
Disparo: useEffect: si !isSignedIn && getAnonCaptionsToday() > 0 → showUpgrade = true.
Contenido: Título "Desbloquea mas captions", cuerpo "Crea una cuenta gratis y obten 3 captions al dia!", botón "Iniciar sesion con Google" (icono Google + SignInButton mode="modal"), botón X para cerrar, backdrop con blur clicable para cerrar.
F3.9 Facturación — gestión de suscripción (Stripe)
Descripción: Página que muestra plan actual, planes disponibles, paquetes de créditos e historial de facturas.
Flujo:
1. Acceso: tab "Facturación" del header (desktop) o tab móvil.
2. useBilling() dispara loadAll() al montar: carga pricing-plans, summary e invoices en paralelo desde la API Stripe.
3. Si no hay token Clerk en localStorage, muestra solo: botón volver, planes (con botones "Inicia sesion" deshabilitados), créditos y un prompt "Inicia sesión para acceder a facturación".
4. Con sesión: tarjeta "Tu Plan" (plan suscrito, estado, fecha de renovación, aviso "Se cancelará al final del período" si aplica, importe/intervalo), grid de PlanCards, paquetes de créditos y tabla de facturas.
5. Suscribirse: POST /api/v1/billing/create-checkout-session con {priceId, successUrl: /billing?success=true, cancelUrl: /billing?canceled=true} → redirige a la URL de Stripe Checkout.
6. Gestionar facturación: POST /api/v1/billing/create-portal-session → redirige al Customer Portal de Stripe (botón visible solo con suscripción).
7. Comprar créditos: POST /api/v1/billing/buy-credits con {packId, successUrl, cancelUrl} → redirige a checkout; mientras procesa el botón muestra "Procesando...".
8. Historial de facturas: tabla con Nº (number o últimos 8 chars del id), fecha formateada, importe + moneda, estado con colores (paid=verde, open=ámbar, uncollectible=rojo, void=gris), enlace "Ver" al hostedUrl de Stripe (nueva pestaña).
Estados: carga (spinner + "Cargando datos de facturación..."), error (banner rojo con botón × para limpiar), sin facturas ("Sin facturas aún"), sin suscripción ("Sin suscripción activa").
F3.10 Perfil — datos de usuario, plan y API keys
Descripción: Muestra info de Clerk, estadísticas de plan/uso y (solo Enterprise) gestión de API keys.
Flujo:
1. Acceso: tab "Perfil" del header.
2. Sin sesión: prompt "Inicia sesión para acceder al perfil" + botón volver.
3. Con sesión:
- Información del Usuario: avatar (imageUrl de Clerk o icono), nombre, email, "Miembro desde" (fecha formateada).
- Plan y Uso: plan (código en mayúsculas), límite diario, usados, restantes (verde), créditos extra (ámbar, de summary.bonus_credits_remaining), estado de acceso API (Activado/No disponible).
- Claves API (solo Enterprise): si no es Enterprise → bloqueo con icono Shield ámbar, "Actualiza a Enterprise", descripción y botón "Ver Planes" (window.location.href='/billing'). Si es Enterprise: botón "Generar Clave API" → input de nombre (placeholder "Ej: Producción, Desarrollo") + confirmar; la clave nueva se muestra una sola vez (mascarada primeros 12 chars + ••••••••, con ojo mostrar/ocultar y botón copiar); lista de claves con nombre, fecha creada, último uso, botones copiar y revocar (doble confirmación: "Revocar" / "Cancelar"); IP whitelist (input separado por comas + "Guardar Lista Blanca", feedback "¡Guardado!" 2s).
4. Endpoints: GET /api/v1/api-keys, POST /api/v1/api-keys/generate {name}, DELETE /api/v1/api-keys/:id, POST /api/v1/api-keys/whitelist {ips}, GET /api/v1/api-keys/whitelist.
F3.11 VersionBadge
Descripción: Badge de versión de la app (componente global, renderizado al final de la página). Muestra la versión del frontend.
F3.12 Enlace a Docs
Descripción: Enlace externo /docs (nueva pestaña) en la navegación desktop y móvil.
4. Pantallas (wireframes textuales)
P4.1 Layout global (header + contenido)
┌────────────────────────────────────────────────────────────────────┐
│ HEADER (border-b, max-w-6xl) │
│ ┌──────────┬───────────────────────────┬────────────────────────┐ │
│ │ [✨] │ Generator│Facturación│ │ [QuotaBadge] [Gratis] │ │
│ │ CaptionAI │ Perfil│Docs │ [avatar] [🌙☀️🖥] [EN|ES] │ │
│ │ AI-powered│ (tabs, desktop) │ o [Iniciar sesion] │ │
│ └──────────┴───────────────────────────┴────────────────────────┘ │
│ MOBILE: tabs horizontales Generator|Facturación|Perfil|Docs │
├────────────────────────────────────────────────────────────────────┤
│ MAIN (max-w-6xl, py-6) │
│ view=generator → P4.2 view=billing → P4.3 view=profile → P4.4 │
├────────────────────────────────────────────────────────────────────┤
│ [UpgradeDialog modal] [VersionBadge] │
└────────────────────────────────────────────────────────────────────┘
P4.2 Vista Generator (grid 3 columnas, lg)
┌───────────────┬──────────────────────────────────────────────┐
│ PANEL CONFIG │ PANEL RESULTADO (col-span-2) │
│ (col-span-1) │ │
│ ┌───────────┐ │ Estado vacío: │
│ │ Configuracion │ │ ┌──────────────────────────────────┐ │
│ │ │ │ │ [📄 icono grande gris] │ │
│ │ Tema │ │ │ Sin caption generado │ │
│ │ [textarea │ │ │ Configura los parametros y pulsa │ │
│ │ "De que…"]│ │ │ "Generar Caption" │ │
│ │ │ │ └──────────────────────────────────┘ │
│ │ Plataforma │ │ │
│ │ (📷Instagram│ │ Estado carga: │
│ │ X│in│🎵│f) │ │ [spinner girando] Generando... │
│ │ Tono │ │ Esto puede tardar unos segundos │
│ │ (pills: │ │ │
│ │ profesional│ │ Estado resultado: │
│ │ casual… ) │ │ ┌──────────────────────────────────┐ │
│ │ Tipo cont. │ │ │ 👁 Generated Caption [📋][🔄] │ │
│ │ (pills: │ │ │ HOOK: [box bg-secondary] │ │
│ │ video… ) │ │ │ BODY: [box texto multi-línea] │ │
│ │ Idioma sal.│ │ │ CTA: [box itálica] │ │
│ │ (ES EN FR │ │ │ COMPLETO: [box borde, texto │ │
│ │ DE ZH JA │ │ │ pre-wrap] │ │
│ │ RU) │ │ │ Hashtags sugeridos: #a #b #c… │ │
│ │ │ │ └──────────────────────────────────┘ │
│ │ [✨ Generar│ │ │
│ │ Caption] │ │ Estado error (encima de todo): │
│ │ (disabled │ │ [banner rojo: mensaje de error] │
│ │ si vacío) │ │ │
│ └───────────┘ │ │
└───────────────┴──────────────────────────────────────────────┘
P4.3 Vista Facturación (Billing)
[← Volver al Generador]
Facturación — Gestiona tu suscripción y facturación [Gestionar facturación ↗] (solo con suscripción)
[banner error rojo con × si hay error]
[spinner + "Cargando datos de facturación..." si loading]
┌─ TU PLAN ───────────────────────────────────────────┐
│ Suscrito a: [Nombre plan] [Activo🟢|Cancelado🟡] │
│ Fecha de renovación: 5 sep 2026 │
│ (Se cancelará al final del período — ámbar, si aplica)│
│ 9.00 EUR/month │
│ — o — "Sin suscripción activa" │
└─────────────────────────────────────────────────────┘
┌─ PLANES DE PRECIOS ────────────────────────────────┐
│ [PlanCard Free] [PlanCard Basic] [PlanCard Pro] │
│ cada card: nombre, precio (Gratis | €X/mes o /año),│
│ features con ✓ (captions diarios, caracteres máx, │
│ idiomas, acceso API, modelo IA), botón Suscribirse │
│ (o "Plan actual" si es el actual, o "Inicia sesión"│
│ si signedOut) │
└─────────────────────────────────────────────────────┘
┌─ PAQUETES DE CRÉDITOS (si existen) ────────────────┐
│ Créditos Extra │
│ [ 1000 ] [ €X ] [Comprar] [ 5000 ] [ €Y ] [Comprar]│
└─────────────────────────────────────────────────────┘
┌─ HISTORIAL DE FACTURAS ────────────────────────────┐
│ Factura # | Fecha | Importe | Estado | Ver │
│ INV-001 | 5 ago 2026 | 9.00 EUR | Activo | Ver ↗ │
│ — o — "Sin facturas aún" │
└─────────────────────────────────────────────────────┘
P4.4 Vista Perfil (max-w-4xl)
[← Volver al Generador]
Perfil — Gestiona la configuración de tu cuenta
┌─ INFORMACIÓN DEL USUARIO ──────────────────────────┐
│ [avatar 48px] Nombre Apellido │
│ email@dominio.com │
│ Nombre: X Email: Y │
│ Miembro desde: 5 ago 2026 │
└────────────────────────────────────────────────────┘
┌─ PLAN Y USO ───────────────────────────────────────┐
│ Plan: FREE | Límite diario: 3 | Usados: 0 │
│ Restantes: 3 (verde) | Créditos Extra: 0 (ámbar) │
│ Acceso API: No disponible / Activado │
└────────────────────────────────────────────────────┘
┌─ CLAVES API (solo Enterprise) ─────────────────────┐
│ Si NO Enterprise: │
│ [🛡 ámbar] Actualiza a Enterprise │
│ "La gestión de claves API está disponible…" │
│ [Ver Planes] │
│ Si Enterprise: │
│ [+ Generar Clave API] → input nombre + confirmar │
│ [banner verde: clave generada, mascarada, 👁 📋] │
│ Lista: nombre · creada · último uso · [📋][🗑] │
│ IP Whitelist: [input "192.168.1.1, 10.0.0.0/24"] │
│ [Guardar Lista Blanca → ¡Guardado!] │
└────────────────────────────────────────────────────┘
P4.5 Modal UpgradeDialog (overlay)
┌───────────── overlay negro blur ──────────────┐
│ ┌──────────────────────────────────────┐ │
│ │ [✨] [X cerrar] │ │
│ │ Desbloquea mas captions │ │
│ │ Crea una cuenta gratis y obten 3 │ │
│ │ captions al dia! │ │
│ │ [G Google Iniciar sesion con Google]│ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
5. Flujos de trabajo
Flujo A: Alta (anónimo → cuenta)
- Usuario llega a la landing (vista Generator por defecto).
- Genera su primer caption como anónimo.
- Al completarse la generación, aparece el UpgradeDialog ("Crea una cuenta gratis y obten 3 captions al dia!").
- Pulsa "Iniciar sesion con Google" → modal Clerk → autenticación (Google/email).
- Tras login, el header muestra QuotaBadge (
0/3), pill "Gratis" y UserButton. El contador anónimo deja de usarse y pasa ausage_log.
Flujo B: Configuración y generación
- En la vista Generator, escribir tema en el textarea.
- Seleccionar plataforma, tono, tipo de contenido e idioma de salida (pills).
- Pulsar "Generar Caption" (habilitado solo si hay tema).
- Ver estado "Generando..." durante la llamada a
/api/v1/caption/generate. - Ver resultado estructurado (Hook / Body / CTA / Completo / Hashtags).
- Copiar con un clic o regenerar con el icono de refresco.
Flujo C: Resultado y reutilización
- Con resultado en pantalla: copiar caption completo al portapapeles (feedback "Copiado!").
- Regenerar variaciones con el icono RefreshCw (mismos parámetros).
- Cambiar un parámetro (p. ej. tono) y volver a generar.
Flujo D: Billing (suscripción)
- Ir a "Facturación" (tab del header o clic en QuotaBadge si plan free).
- Ver planes en PlanCards; pulsar "Suscribirse" en el deseado.
- Redirección a Stripe Checkout → pago.
- Vuelta a
/billing?success=true→ el summary refleja la suscripción (Tarjeta "Tu Plan" con estado Activo, fecha de renovación, importe). - Opcional: "Gestionar facturación" abre el Customer Portal de Stripe (cambiar tarjeta, cancelar).
- Opcional: comprar paquetes de créditos extra ("Comprar" → checkout → los créditos se reflejan en
bonus_credits_remainingdel perfil).
Flujo E: Perfil y API keys (Enterprise)
- Ir a "Perfil".
- Revisar info de usuario y consumo del plan.
- Si plan Enterprise: generar API key con nombre, copiarla (solo visible una vez completa), gestionar whitelist de IPs, revocar claves no usadas (con confirmación).
6. Reglas de negocio
| Regla | Detalle |
|---|---|
| Quota anónima | Los usuarios anónimos no tienen límite duro en frontend, pero tras el primer caption del día (contador anon_captions en localStorage por fecha) se muestra el UpgradeDialog cada vez que generan. |
| Quota logueados (plan Free) | 3 captions/día por defecto (QuotaBadge). El contador se lleva en localStorage.usage_log (clave por fecha YYYY-MM-DD). El límite real se obtiene del plan en Stripe (limits.daily_captions). Nota: usePermissions.FREE_LIMITS define 5/día (valor distinto al default del badge — discrepancia interna a resolver). |
| Límites por plan | PricingPlan.limits: daily_captions, max_characters, languages, has_api, extra_rate, ai_model. El plan Free: has_api: false. La UI de PlanCard muestra: captions diarios, caracteres máx, idiomas, acceso API (Sí/No), modelo IA. |
| Acceso API | Solo planes con has_api: true (Enterprise según perfil). La gestión de API keys está bloqueada para no-Enterprise con pantalla "Actualiza a Enterprise". |
| Suscripción | Se detecta vía GET /api/v1/billing/summary (hasSubscription, subscription.status). Estado active = verde; resto = ámbar. cancelAtPeriodEnd muestra aviso "Se cancelará al final del período". |
| Créditos extra | Compras one-shot vía buy-credits; se muestran como bonus_credits_remaining en el perfil. Los precios de créditos se muestran en EUR. |
| Facturas | Estados mapeados: paid (verde), open (ámbar), uncollectible (destructivo), void (gris). El nº de factura usa number o los últimos 8 caracteres del id. |
| Auth | Clerk gestiona sesión; el JWT se persiste en localStorage.clerk_token y se envía como Authorization: Bearer a los endpoints /api/v1/* de billing y api-keys. |
| Tema UI | Solo afecta a la clase dark en <html>; valores: dark (default), light, system (sigue a prefers-color-scheme). |
| Idioma UI | Default es; persistido en localStorage.lang. Los arrays de tono/tipo se traducen según el idioma de UI. |
Endpoints consumidos por el frontend
POST /api/v1/caption/generate— generación de captions.GET /api/v1/billing/pricing-plans— planes de precios.POST /api/v1/billing/create-checkout-session— checkout Stripe{priceId, successUrl, cancelUrl, couponCode?}.POST /api/v1/billing/create-portal-session— Customer Portal{returnUrl}.GET /api/v1/billing/summary— resumen de suscripción/cliente.GET /api/v1/billing/invoices— historial de facturas.POST /api/v1/billing/create-user-profile— creación de perfil{email}.GET /api/v1/billing/credit-packs— paquetes de créditos.POST /api/v1/billing/buy-credits— compra de créditos{packId, successUrl, cancelUrl}.GET/POST/DELETE /api/v1/api-keys[/generate|/:id|/whitelist]— gestión de API keys (Enterprise).