MEMORIA FUNCIONAL — ThumbnailGen
Producto: ThumbnailGen — Generador de thumbnails con IA
URL live: https://thumbnailgen.theboomer.dev
Frontend: code/frontend/src/App.tsx (+ pages/, components/, services/, stores/, hooks/)
Backend: FastAPI (/api/v1/thumbnail/*); auth Clerk; billing vía tentpole-stripe-api
Fuente: Documento generado a partir del código real del frontend (App.tsx, QuotaBadge, UpgradeDialog, Billing, Profile, History, PlanCard, usePermissions, stripe.service) y el spec docs/03-MEMORIA_FUNCIONAL.md.
1. Introducción
ThumbnailGen es una aplicación web SPA (React + Vite + Tailwind + Clerk) que genera thumbnails para vídeo usando IA. El usuario describe el tema de su vídeo, elige plataforma, estilo visual y tipo de contenido, y el sistema devuelve una imagen PNG lista para descargar. Incluye autenticación (Clerk), cuotas diarias por plan, historial de generaciones, facturación con Stripe (suscripciones + paquetes de créditos) y gestión de API keys (solo Enterprise).
El idioma de la UI es configurable (ES/EN, default es) y el tema visual (dark/light/system, default dark), ambos persistidos en localStorage.
2. Tipos de usuario
| Tipo | Cómo se identifica | Acceso |
|---|---|---|
| Usuario anónimo | No autenticado (SignedOut) |
Puede generar 1 thumbnail/día (control por localStorage anon_thumbnails en frontend + IP en backend). Tras la 1ª generación se muestra el diálogo de upgrade. |
| Usuario registrado (Free) | Sesión Clerk (SignedIn) |
3 thumbnails/día (límite del plan free). Badge de cuota usados/límite en el header. |
| Usuario Pro | Suscripción Stripe activa | 50 thumbnails/día (según límites del plan devueltos por stripe-api). |
| Usuario Enterprise | Suscripción Enterprise | Ilimitado + API keys con IP whitelist (gestión exclusiva de este plan). |
Nota: el backend consulta el plan del usuario a stripe-api (/api/v1/billing/internal/plan/{clerk_id}) y aplica límites diarios por plan; el frontend muestra la cuota aproximada en el QuotaBadge usando localStorage('usage_log') + los límites del plan.
3. Funcionalidades
F-101 Autenticación con Clerk
- Descripción: Login/registro vía Clerk (modal con Google SSO). El token JWT se guarda en
localStorage('clerk_token')y se envía comoAuthorization: Beareren cada llamada a la API. - Flujo: 1) Clic en "Iniciar sesión" (header) → 2) modal Clerk → 3) sesión creada → 4)
useClerkToken()refresca y guarda el token → 5) la UI cambia aSignedIn(UserButton + QuotaBadge + badge "Gratis"). - Validaciones: Solo usuarios autenticados pueden acceder a Billing/Profile (si no hay token, se muestra pantalla "Inicia sesión para acceder a facturación/perfil").
afterSignOutUrl="/".
F-102 Cambio de idioma (ES/EN)
- Descripción: Selector EN/ES en el header. ~30 strings por idioma (título, subtítulo, labels, placeholders, estados, botones).
- Flujo: Seleccionar idioma →
setLang()→ persistencia enlocalStorage('lang')→ re-render con las cadenasi18n[lang]. - Default:
es.
F-103 Tema visual (dark / light / system)
- Descripción: Selector de 3 botones (luna/sol/monitor) en el header.
- Flujo: Clic →
useTheme()aplica la clasedarkal<html>(resolviendosystemconprefers-color-scheme) y persiste enlocalStorage('theme'). Escucha cambios del media query cuando está ensystem.
F-104 Generación de thumbnail
- Descripción: Núcleo del producto. El usuario configura parámetros y recibe una imagen generada.
- Flujo:
1. Rellenar el textarea Tema (
¿De qué trata tu video?) — obligatorio. 2. Seleccionar Plataforma (pills en grid 2×2): YouTube, Instagram, TikTok, X/Twitter. Default:youtube. 3. Seleccionar Estilo Visual (pills): profesional, gaming, minimalista, colorido, oscuro, educativo, vlog. Default:profesional. 4. Seleccionar Tipo de contenido (pills): tutorial, gaming, vlog, review, unboxing, podcast, educativo, entretenimiento. Default:tutorial. 5. Clic en "Generar Thumbnail" (deshabilitado si no hay tema o mientras carga). 6.POST /api/v1/thumbnail/generatecon body{ description: tema, template_id: "default", text: tema }+ Bearer si hay sesión. 7. Éxito →setResult({ id, image_b64, download_url }); se muestra el panel de resultado. Anónimo:incrementAnonThumbnails()y abre el UpgradeDialog. Autenticado:incrementTodayUsage()(localStorageusage_log). 8. Se refresca el historial (GET /api/v1/thumbnail/list). - Validaciones (frontend):
temano vacío (botón deshabilitado con!tema.trim()). El backend exige ≥3 caracteres. - Estados:
loading(spinner + "Esto puede tardar unos segundos"),error(banner rojo conerrData.detail),result(imagen + acciones), vacío ("Sin thumbnail generado"). - Errores backend: 400 validación, 429 cuota excedida (
quota_exceeded/quota_exceeded_anon), 500 error interno.
F-105 Descargar thumbnail
- Descripción: Descarga local de la imagen generada.
- Flujo: Botón "Descargar" (icono Download) → enlace
data:image/png;base64,...condownload="thumbnail-{id}.png". - Validaciones: Solo visible si
result.image_b64existe.
F-106 Crear nuevo thumbnail
- Descripción: Limpia el resultado actual para empezar otra generación.
- Flujo: Botón "Crear Nuevo" (icono RefreshCw) →
setResult(null); setTema('').
F-107 Upgrade dialog para anónimos
- Descripción: Modal que invita a crear cuenta tras la primera generación anónima.
- Disparo:
!isSignedIn && getAnonThumbnailsToday() > 0(localStorageanon_thumbnailspor día). - UI: Título "Desbloquea mas thumbnails", cuerpo "Crea una cuenta gratis y obten 3 thumbnails al dia!", botón "Iniciar sesion con Google" (Clerk modal) y botón Cerrar (X / backdrop click).
F-108 Quota badge
- Descripción: Badge en el header (solo autenticados) con
usados/límite(ej.2/3). - Flujo: Carga
getPricingPlans()+getSummary()→ busca el plan porstripeProductId→ límiteplan.limits.daily_captions(free=3). Uso leído delocalStorage('usage_log'). - Interacción: Si el plan es
free, clic → vista Billing ("Ver planes"). Si es de pago, es informativo ("N restantes").
F-109 Historial (inline en Generator + página History)
- Descripción: Lista las thumbnails generadas. En la vista Generator se muestra un panel "Historial" con los últimos 10 items; existe además una página History dedicada.
- Flujo:
GET /api/v1/thumbnail/list(Bearer si hay sesión) →data.items→ lista con placeholder visual "Thumb",template_name || idyplatform. Vacío: "Sin thumbnails aún". - Nota de código: la página History (
pages/history/History.tsx) renderiza buscador "Buscar por tema..." y lista con descarga, pero su lista local está inicializada vacía (useState([])), por lo que hoy muestra siempre el estado vacío; el listado real visible es el panel inline del Generator.
F-110 Planes y suscripción (Billing)
- Descripción: Vista de facturación con planes de precio, plan actual, facturas y paquetes de créditos.
- Flujo:
1.
useBilling().loadAll()cargapricingPlans,summary,invoices(vía stripe-api). 2. Tu Plan: si hay suscripción muestra plan, estado (Activo/Cancelado/Vencido/Incompleto/No pagado), fecha de renovación, importe/intervalo y aviso "Se cancelará al final del período" sicancelAtPeriodEnd. Sin suscripción: "Sin suscripción activa". 3. Planes de Precios: cardsPlanCard(soloisActive) con nombre, precio (€/mes o €/año, o "Gratis"), features (thumbnails diarios, caracteres máx., idiomas, acceso API, modelo IA) y botón "Suscribirse" →createCheckoutSession(priceId)→ redirect a Stripe. 4. Gestionar facturación: si hay suscripción →createPortalSession()→ Stripe Customer Portal. 5. Paquetes de Créditos:GET /api/v1/billing/credit-packs→ cards con nº créditos, precio € y botón "Comprar" →buyCredits(packId)→ redirect. 6. Historial de Facturas: tabla (Nº, fecha, importe, estado, enlace "Ver" ahostedUrl). - Validaciones: si no existe
localStorage('clerk_token')se muestra el bloqueo "Inicia sesión para acceder a facturación" (con los planes visibles para invitar al registro).
F-111 Perfil de usuario
- Descripción: Información de cuenta, plan/uso y API keys (Enterprise).
- Flujo:
1. Información del Usuario: avatar, nombre, email, "Miembro desde" (desde Clerk).
2. Plan y Uso: plan (código), límite diario, usados, restantes (verde), Créditos Extra (
summary.bonus_credits_remaining), Acceso API (Activado/No disponible). 3. Claves API (solo Enterprise): si el plan no esenterprise→ bloqueo "Actualiza a Enterprise" con botón "Ver Planes" (→/billing). Si es Enterprise: botón "Generar Clave API" (nombre obligatorio, Enter o clic), la clave se muestra una sola vez (oculta con••••••••, toggle ojo), botones copiar; lista de keys con fecha creación/último uso, copiar y revocar (con confirmación "¿Estás seguro...? Esta acción no se puede deshacer."); Lista Blanca de IPs (texto separado por comas + "Guardar Lista Blanca", feedback "¡Guardado!"). - Validaciones: requiere sesión (si no → "Inicia sesión para acceder al perfil").
4. Pantallas (wireframes textuales)
P1 — Header (común a todas las vistas)
┌────────────────────────────────────────────────────────────────────────────┐
│ [🖼 ThumbnailGen ] Generator | Historial | Perfil | Facturación │
│ Generador de thumbnails│ [2/3 ⬢free]│
│ con IA │ [👤] [🌙☀️🖥] [EN ▾] ó [Iniciar sesión] │
└────────────────────────────────────────────────────────────────────────────┘
- Izquierda: logo + nombre + subtítulo + tabs de navegación (desktop).
- Derecha (autenticado): QuotaBadge (
usados/límite), badge "Gratis", UserButton de Clerk, selector tema, selector idioma. - Derecha (anónimo): botón "Iniciar sesión".
- Móvil: la navegación pasa a pestañas inferiores (Generator | Historial | Perfil | Facturación).
P2 — Generator (vista principal, grid 3 columnas)
┌───────────────┬────────────────────────────────────────────────────────────┐
│ Configuración │ [Error banner rojo si error] │
│ │ [Loading: spinner + "Esto puede tardar unos segundos"] │
│ Tema │ ┌──────────────────────────┐ │
│ textarea │ │ Thumbnail Generado │ ← panel resultado │
│ (¿De qué trata│ │ [imagen PNG full-width] │ (solo si result) │
│ tu video?) │ │ [Descargar] [Crear Nuevo]│ │
│ │ └──────────────────────────┘ │
│ Plataforma │ ┌──────────────────────────┐ │
│ [▶ YouTube] │ │ Sin thumbnail generado │ ← estado vacío inicial │
│ [📷 Instagram]│ │ Configura los parámetros │ (solo si !result) │
│ [🎵 TikTok] │ │ y pulsa "Generar..." │ │
│ [💬 X/Twitter]│ └──────────────────────────┘ │
│ │ ┌──────────────────────────┐ │
│ Estilo Visual │ │ Historial │ │
│ pills: │ │ [Thumb] tema1 · youtube │ ← últimos 10 items │
│ profesional, │ │ [Thumb] tema2 · instagram│ o "Sin thumbnails aún" │
│ gaming, ... │ └──────────────────────────┘ │
│ │ │
│ Tipo contenido│ │
│ pills: │ │
│ tutorial, ... │ │
│ │ │
│ [✨ Generar │ │
│ Thumbnail] │ │
└───────────────┴────────────────────────────────────────────────────────────┘
- Columna izquierda (1/3): panel "Configuración" con tema (textarea 20vh), plataforma (grid 2×2), estilo y tipo (pills), botón de generación full-width.
- Columna derecha (2/3): error / loading / resultado / estado vacío / historial.
P3 — Billing (Facturación)
← Volver al Generador
Facturación — Gestiona tu suscripción y facturación [🔗 Gestionar facturación si suscripción]
┌─────────────────────────────────────────┐
│ Tu Plan │
│ Suscrito a: Pro [Activo] │
│ Fecha de renovación: 05 sep 2026 │
│ (aviso ámbar si cancelAtPeriodEnd) │
│ 9,00 €/month │
│ — o — "Sin suscripción activa" │
└─────────────────────────────────────────┘
Planes de Precios
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Free │ │ Pro │ │ Enterprise │
│ Gratis │ │ 9,00 €/mes │ │ 29,00 €/mes │
│ ✓ 3 thumbnails│ │ ✓ 50 thumbnails│ │ ✓ Ilimitado │
│ diarios │ │ ... │ │ ... │
│ [Suscribirse] │ │ [Suscribirse] │ │ [Suscribirse] │
└───────────────┘ └───────────────┘ └───────────────┘
Paquetes de Créditos — Créditos Extra
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 1.000 │ │ 5.000 │ │ 10.000 │
│ créditos │ │ créditos │ │ créditos │
│ €9,99 │ │ €39,99 │ │ €69,99 │
│ [Comprar] │ │ [Comprar] │ │ [Comprar] │
└────────────┘ └────────────┘ └────────────┘
Historial de Facturas
┌───────────┬──────────┬────────┬───────┬──────┐
│ Factura # │ Fecha │ Importe│ Estado│ │
│ inv_... │ 05 ago │ 9,00 € │ Paid │ [Ver]│
└───────────┴──────────┴────────┴───────┴──────┘
(o "Sin facturas aún")
- Sin token: bloque central "Inicia sesión para acceder a facturación" + botón volver (los planes y credit packs siguen visibles).
P4 — Profile (Perfil)
← Volver al Generador
Perfil — Gestiona la configuración de tu cuenta
┌─────────────────────────────────────────────┐
│ Información del Usuario │
│ [avatar] Nombre Apellidos │
│ email@example.com │
│ Nombre: X · Email: X · Miembro desde: X │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Plan y Uso │
│ Plan: FREE · Límite diario: 3 · Usados: 1 │
│ Restantes: 2 (verde) · Créditos Extra: X │
│ Acceso API: Activado / No disponible │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Claves API (Enterprise only) │
│ ┌─ si NO enterprise ─────────────────────┐ │
│ │ 🛡 Actualiza a Enterprise │ │
│ │ "La gestión de claves API está ..." │ │
│ │ [Ver Planes] │ │
│ └────────────────────────────────────────┘ │
│ ┌─ si enterprise ────────────────────────┐ │
│ │ [➕ Generar Clave API] │ │
│ │ (form: nombre + Generar + ×) │ │
│ │ [clave nueva visible una vez 🟢] │ │
│ │ Lista: nombre · creada · último uso │ │
│ │ [copiar] [revocar] │ │
│ │ Lista Blanca de IPs: input + Guardar │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
P5 — History (Historial)
← Volver
Historial de Contenido — Tu contenido generado anteriormente
[🔍 Buscar por tema...]
(siempre estado vacío en la implementación actual: "Aún no has generado contenido")
P6 — UpgradeDialog (modal global)
┌───────────────────────────────┐
│ [×] │
│ ✨ │
│ Desbloquea mas thumbnails │
│ "Crea una cuenta gratis y │
│ obten 3 thumbnails al dia!" │
│ [G Iniciar sesion con Google]│
└───────────────────────────────┘
(backdrop blur; clic fuera cierra)
5. Flujos de trabajo
Flujo 1 — Anónimo genera su primer thumbnail
- Entra a la app (estado vacío, botón "Iniciar sesión" en header).
- Rellena Tema, elige plataforma/estilo/tipo y pulsa "Generar Thumbnail".
- Ve el spinner ("Esto puede tardar unos segundos") y luego el resultado con "Descargar".
incrementAnonThumbnails()→ se abre el UpgradeDialog ("Crea una cuenta gratis...").- Opción A: cierra el diálogo → ha gastado su generación del día (el contador
anon_thumbnailspersiste por día; el backend limita a 1/día por IP). - Opción B: "Iniciar sesión con Google" → modal Clerk → pasa a autenticado (Free, 3/día).
Flujo 2 — Registrado Free genera y descarga
- Login Clerk → header muestra QuotaBadge
0/3, badge "Gratis" y avatar. - Configura y genera → resultado visible; QuotaBadge pasa a
1/3(localStorageusage_log). - Descarga PNG (
thumbnail-{id}.png) o pulsa "Crear Nuevo". - El panel Historial se actualiza con el nuevo item.
- Al agotar el límite, el backend responde 429
quota_exceeded→ banner de error; clic en QuotaBadge → Billing para hacer upgrade.
Flujo 3 — Upgrade a Pro/Enterprise
- Pestaña Facturación → "Planes de Precios".
- "Suscribirse" en un plan → checkout de Stripe → pago → webhook actualiza el plan.
- De vuelta, "Tu Plan" muestra la suscripción activa; QuotaBadge muestra el nuevo límite diario.
- Enterprise adicionalmente desbloquea la sección de API keys en Perfil (generar → copiar una sola vez → usar en integraciones; whitelist de IPs).
6. Reglas de negocio
Cuotas y límites (back-end vía stripe-api; frontend como referencia)
| Usuario | Límite diario | Control |
|---|---|---|
| Anónimo | 1 thumbnail/día | IP (backend) + localStorage('anon_thumbnails') (frontend) |
| Free | 3 thumbnails/día | plan.limits.daily_captions (stripe-api); usage_log en frontend |
| Pro | 50 thumbnails/día | idem |
| Enterprise | Ilimitado | idem |
- La cuota se verifica antes de cada generación (
check_hashtag_quota/rate-limiter → stripe-api). - Respuesta 429:
{ code: "quota_exceeded"|"quota_exceeded_anon", message (ES), limit, used, plan, upgrade_url: "/billing" }. - Si el plan tiene
extra_rate > 0se permite el exceso con cargo; si hay créditos bonus se consumen antes de rechazar (consume-bonus-credit). - El uso diario del frontend se reinicia por fecha ISO (
usage_logyanon_thumbnailsguardan{date, count}).
Validaciones de entrada
- Tema: obligatorio en frontend; backend: mínimo 3 caracteres, máx. 200 (sanitizado).
- Plataforma: youtube / instagram / tiktok / twitter. Estilo y tipo: catálogos cerrados (validación 400 si inválido).
template_idfijo"default"en la UI actual.
API (endpoints usados por el frontend)
| Método | Endpoint | Uso |
|---|---|---|
| POST | /api/v1/thumbnail/generate |
Generar (body: {description, template_id, text}) → {id, image_b64, download_url} |
| GET | /api/v1/thumbnail/list |
Listar historial → {items: [{id, template_name, platform, ...}]} |
| GET | /api/v1/billing/pricing-plans |
Planes (con limits.daily_captions, max_characters, languages, has_api, ai_model, extra_rate) |
| GET | /api/v1/billing/summary |
Resumen suscripción + bonus_credits_remaining |
| GET | /api/v1/billing/invoices |
Facturas |
| POST | /api/v1/billing/create-checkout-session |
Checkout Stripe |
| POST | /api/v1/billing/create-portal-session |
Portal Stripe |
| GET | /api/v1/billing/credit-packs |
Paquetes de créditos |
| POST | /api/v1/billing/buy-credits |
Comprar créditos (redirect a Stripe) |
| GET/POST | /api/v1/auth/me, /api/v1/auth/sync |
Perfil y sync Clerk→Mongo |
| GET/POST/DELETE | /api/v1/api-keys* |
Gestión API keys (Enterprise) |
Facturación
- Moneda EUR; planes con intervalo mes/año; suscripciones gestionadas en el Stripe Customer Portal.
- Estados de suscripción: active, canceled, past_due, incomplete, unpaid (mapeados a ES: Activo/Cancelado/Vencido/Incompleto/No pagado).
- Estados de factura: paid/open/uncollectible/void.
Notas técnicas
- Auth: token Clerk en
localStorage('clerk_token'); el servicio intenta refrescarlo desdewindow.Clerk.session.getToken()(polling hasta 10s) antes de cada llamada. - El backend
code/app/main.pyes una versión demo/legacy (almacenamiento en memoria,/api/generate, placeholders SVG); el backend desplegado con/api/v1/thumbnail/*sigue el spec dedocs/03-MEMORIA_FUNCIONAL.md.