Volver a la biblioteca
Automatizaciones Nivel Intermedio 20 de julio de 2026

Crea tu propio panel de estudio con IA

Aprende a crear un panel de estudio local con IA que resume PDFs, organiza sesiones y genera estructuras de estudio automáticamente.

Claude Code Visual Studio Code
Video del recurso

En el video verás el panel funcionando y cómo generarlo completo con un solo prompt.

Ver en Instagram

Sobre este recurso

Estudiar documentos extensos puede consumir mucho tiempo, especialmente cuando necesitas identificar los conceptos importantes, crear resúmenes y organizar la información para prepararte mejor.

En este video te enseño cómo crear tu propio panel de estudio local utilizando Claude Code. El sistema permite subir documentos en formato PDF y utiliza inteligencia artificial para extraer el contenido, generar un resumen completo y crear automáticamente una estructura de estudio con conceptos clave, orden recomendado y preguntas de práctica.

También aprenderás a construir una interfaz donde puedas guardar diferentes sesiones, buscar documentos anteriores, cambiar sus nombres, eliminarlos y volver a consultar sus resúmenes sin procesar nuevamente los archivos.

Todo el sistema funciona localmente en tu computadora y guarda la información dentro de la misma carpeta del proyecto, sin utilizar bases de datos externas ni servicios de almacenamiento en la nube. El panel utiliza Claude Code como motor de inteligencia artificial, sin necesidad de configurar una API key adicional.

Este recurso es ideal para estudiantes, investigadores, profesionales o cualquier persona que quiera transformar documentos extensos en materiales de estudio claros, organizados y fáciles de consultar.

Paso 1 — Instala Claude Code

El video de instalación para PC o Mac está en los enlaces de este recurso.

Paso 2 — Abre Claude Code en una carpeta vacía

claude --dangerously-skip-permissions

Paso 3 — El Prompt Maestro

Cópialo tal cual, sin borrar nada.

Construye un panel de estudio 100% LOCAL y FUNCIONAL dentro de ESTA carpeta (vacía).
No uses servicios en la nube ni bases de datos externas: TODO se guarda como archivos
dentro de esta misma carpeta usando rutas relativas (./). Al terminar debe quedar LISTO
para correr: dependencias instaladas, todas las carpetas creadas y el servidor verificado
arrancando sin errores. Usa exactamente lo que se describe a continuación; no cambies
nombres, colores, textos ni comportamientos.

═══════════════════════════════════════════════
0. MOTOR DE IA (sin API key externa)
═══════════════════════════════════════════════
Para generar texto NO uses ninguna API key. Usa Claude Code en modo headless: desde el
backend ejecuta `claude -p "<prompt>"` con subprocess y captura su stdout. Reutiliza el
login de Claude Code ya existente. Antes de construir, verifica que el comando `claude`
existe en el PATH (ej. `claude -p "responde solo OK"`).

═══════════════════════════════════════════════
1. STACK Y DEPENDENCIAS (instálalas tú)
═══════════════════════════════════════════════
- Backend: Python 3 + Flask.
- Extracción de PDF: pdfplumber.
- Frontend: HTML + CSS + JavaScript vanilla (sin frameworks, sin build, sin librerías CDN).
- Crea un entorno virtual e instala ahí las dependencias:
    python3 -m venv venv
    ./venv/bin/pip install --upgrade pip
    ./venv/bin/pip install flask pdfplumber
- Crea requirements.txt con dos líneas: `flask` y `pdfplumber`.

═══════════════════════════════════════════════
2. ESTRUCTURA DE ARCHIVOS Y CARPETAS (créala toda)
═══════════════════════════════════════════════
./app.py                  (servidor Flask, único punto de arranque)
./requirements.txt
./README.md
./venv/                   (entorno virtual con las dependencias)
./static/index.html
./static/style.css
./static/app.js
./data/                   (MEMORIA en disco; nada vive en memoria volátil)
./data/sesiones/
./data/indice.json        (si no existe, inicialízalo como lista vacía: [])

Al iniciar el servidor, crea automáticamente ./data/, ./data/sesiones/ y, si no existe,
./data/indice.json con contenido []. Usa una función `asegurar_estructura()` que se llame
al arrancar y también antes de procesar cada subida.

═══════════════════════════════════════════════
3. SERVIDOR (app.py)
═══════════════════════════════════════════════
- Calcula BASE_DIR a partir de la ubicación de app.py y deriva DATA_DIR, SESIONES_DIR,
  INDICE_PATH y STATIC_DIR de forma relativa. Crea Flask con `static_folder=None` (las
  estáticas se sirven manualmente para controlar las rutas).
- Se levanta con UN SOLO comando: `./venv/bin/python app.py`
- Escucha en host 127.0.0.1, puerto 5050, debug=False (NO uses el 5000: en macOS lo ocupa
  el AirPlay Receiver). Al arrancar imprime un banner con el nº de sesiones cargadas desde
  disco y la URL http://127.0.0.1:5050.
- Persistencia: funciones `leer_indice()` (lee ./data/indice.json; si no existe o está
  corrupto devuelve []; valida que sea lista) y `guardar_indice(lista)` (escribe JSON con
  ensure_ascii=False, indent=2).
- `slugify(texto)`: normaliza NFKD → ascii, minúsculas, reemplaza lo no alfanumérico por
  guiones, recorta guiones; si queda vacío devuelve "documento".
- Extracción: `extraer_texto_pdf(ruta)` abre con pdfplumber, concatena `extract_text()` de
  cada página (omitiendo vacías) separando por doble salto de línea.
- IA: constante MAX_CHARS_PROMPT = 45000. `llamar_claude(instruccion, contenido)` recorta
  el contenido a ese límite y arma un prompt con esta forma:
    "<instruccion>\n\n=== TEXTO DEL DOCUMENTO (delimitado) ===\n<texto>\n=== FIN DEL
     DOCUMENTO ===\n\nResponde en espanol, en formato Markdown limpio. No incluyas
     preambulos ni comentarios sobre la tarea, entrega directamente el contenido
     solicitado."
  Ejecuta `subprocess.run(["claude", "-p", prompt], capture_output=True, text=True,
  timeout=300)`. Maneja FileNotFoundError (claude no está en PATH), TimeoutExpired y
  returncode != 0 devolviendo un mensaje de error en cursiva Markdown. Si todo va bien
  devuelve stdout .strip().
- `generar_resumen(texto)`: instrucción = "Eres un asistente de estudio. Lee el siguiente
  documento y redacta un RESUMEN completo que capture TODAS las partes importantes: ideas
  principales, definiciones, datos clave, procesos y conclusiones. Organizalo con
  encabezados y vinetas, condensado pero sin perder nada esencial. Comienza directamente
  con el resumen."
- `generar_estructura(texto)`: instrucción que pide EXACTAMENTE tres secciones Markdown:
  "## Orden de estudio" (lista numerada con el orden recomendado y por qué),
  "## Conceptos clave a dominar" (lista de conceptos imprescindibles con explicación de una
  línea) y "## Preguntas de practica tipo examen" (5 a 10 preguntas mezcla de opción
  múltiple y abiertas). Que comience directamente con la primera sección.

ENDPOINTS (todos):
- GET  /                        → sirve ./static/index.html
- GET  /static/<path:nombre>    → sirve archivos de ./static/
- GET  /api/sesiones            → devuelve el contenido de ./data/indice.json (lista)
- GET  /api/sesion/<sid>        → busca la entrada en el índice por id; si no existe 404;
                                  si existe devuelve JSON con id, titulo, fecha, el
                                  contenido de resumen.md y estructura.md leídos del disco,
                                  y pdf_url. NO reprocesa el PDF.
- GET  /api/sesion/<sid>/pdf    → envía el original.pdf de esa sesión (mimetype
                                  application/pdf); 404 si no existe.
- POST /api/subir               → recibe el PDF en el campo de formulario "archivo" y
                                  RESPONDE EN STREAMING NDJSON (ver sección 7).
- POST /api/sesion/<sid>/renombrar → body JSON {"titulo": "..."}; renombra (ver sección 9).
- DELETE /api/sesion/<sid>      → elimina la sesión completa (ver sección 9).

═══════════════════════════════════════════════
4. PALETA DE COLORES — IDENTIDAD SINERGIA
═══════════════════════════════════════════════
Al inicio del CSS define EXACTAMENTE estas variables en :root y úsalas en todo el panel.
Tema OSCURO. Acento principal cyan; botones y títulos de marca usan el gradiente.

  :root {
    --color-fondo:        #030712;  /* fondo principal (navy casi negro) */
    --color-superficie:   #0A1628;  /* sidebar y tarjetas */
    --color-superficie-2: #060E1A;  /* fondos alternos / hover */
    --color-primario:     #00D4FF;  /* acento principal (cyan SinergIA) */
    --color-primario-2:   #0099FF;  /* segundo tono del gradiente */
    --color-acento:       #10B981;  /* verde esmeralda (exito/estados ok) */
    --color-texto:        #E6FAFF;  /* texto principal */
    --color-texto-tenue:  #CBD5E1;  /* texto secundario */
    --color-borde:        rgba(0,212,255,0.15); /* bordes sutiles cyan */
    --gradiente-marca:    linear-gradient(135deg, #00D4FF 0%, #0099FF 100%);
  }

Aplicación de los colores:
- Fondo general (html, body): --color-fondo. Texto base: --color-texto. Tipografía del
  sistema (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
  sans-serif), 15px, line-height 1.6.
- Sidebar y tarjetas/superficies: --color-superficie. Fondos alternos/inputs/hover:
  --color-superficie-2. Bordes sutiles: --color-borde.
- --gradiente-marca SE APLICA en: el botón "+ Subir documento", la barra superior (topbar)
  y el título de marca "SinergIA" (con background-clip:text para texto degradado). El texto
  sobre los elementos con gradiente es #02121c.
- Acento --color-primario en: foco de inputs, pestaña activa, h3 del contenido, enlaces,
  borde de la sesión activa, hover de botones de acción, relleno de la barra de progreso
  (que usa el gradiente).
- Regla GLOBAL imprescindible: `[hidden] { display: none !important; }` para que el atributo
  hidden gane aunque el elemento tenga display:flex (si falta, el spinner/estado de carga se
  vería siempre).

═══════════════════════════════════════════════
5. MARCA DE AGUA (única y sutil)
═══════════════════════════════════════════════
Un solo elemento fijo con el texto "Hecho con SinergIA", position:fixed abajo a la derecha
(right:14px; bottom:12px), font-size 11px, opacity 0.4, color --color-texto-tenue,
pointer-events:none, z-index 50. Sin logo de fondo. Debe percibirse, no tapar contenido.

═══════════════════════════════════════════════
6. EL PANEL (interfaz) — static/index.html + style.css + app.js
═══════════════════════════════════════════════
Layout: contenedor flex a pantalla completa (height:100vh, overflow hidden) con dos zonas:

A) BARRA LATERAL IZQUIERDA (ancho fijo 300px, fondo --color-superficie):
   - Encabezado de marca: "SinergIA" (h1, 22px, peso 800, texto con gradiente) y subtítulo
     "Panel de estudio".
   - Botón "+ Subir documento" (gradiente de marca, bordes redondeados, sombra cyan, hover
     que lo eleva). Este botón SE QUEDA SIEMPRE visible en todos los estados. Dispara un
     <input type="file" accept="application/pdf" hidden>.
   - Buscador: input de texto con placeholder "Buscar sesion..." que filtra la lista por
     título en vivo (case-insensitive).
   - Lista de sesiones (scrollable). Cada sesión es una tarjeta con: bloque principal
     (título en una línea con ellipsis + fecha en gris) y, A LA DERECHA, dos botones de
     acción que aparecen SOLO al pasar el cursor (opacity 0 → 1 en hover, y siempre visibles
     en la sesión activa): "✎" (Renombrar) y "🗑" (Eliminar). El hover de eliminar se pone
     rojo (#ff6b6b). La sesión activa lleva borde --color-primario y fondo cyan tenue.
   - Si no hay sesiones: mensaje centrado "Aun no hay sesiones. Sube tu primer PDF.". Si el
     buscador no encuentra nada: "Sin resultados para tu busqueda.".

B) VISTA CENTRAL (flex:1, columna):
   - Barra superior (topbar) con gradiente de marca; muestra el título del documento abierto
     o "Selecciona o sube un documento" por defecto.
   - Debajo, TRES secciones mutuamente excluyentes (se alternan con el atributo hidden; solo
     una visible a la vez): #dropzone, #cargando y #resultado.

ESTADOS DE LA INTERFAZ:
   - ESTADO VACÍO (sin documento abierto / nada subido): se muestra SOLO la zona de arrastre
     #dropzone: una tarjeta centrada con borde discontinuo, ícono "⬆", título "Arrastra un
     PDF aqui", texto "o usa el boton Subir documento." y una pista "Se genera un resumen y
     una estructura de estudio, y se guarda localmente en este equipo.". Al arrastrar un PDF
     encima se resalta (outline cyan discontinuo + fondo tenue).
   - DOCUMENTO ABIERTO (al hacer clic en una sesión o al terminar de subir): la zona de
     arrastre DESAPARECE por completo y se muestra #resultado ocupando TODA el área principal
     (NO un recuadro chico fijo). El contenido scrollea como una página completa normal; las
     pestañas quedan sticky arriba. El cuerpo del documento usa ancho 100% con max-width
     920px centrado y padding cómodo, sin marco/tarjeta pequeña.
   - PROCESANDO (solo mientras se sube/arrastra un PDF nuevo): se muestra #cargando con una
     tarjeta de progreso (spinner + texto de estado + barra de progreso + porcentaje). En el
     estado normal NUNCA debe verse este indicador.

   #resultado contiene dos pestañas: "Resumen" y "Estructura de estudio". Solo una de las dos
   áreas (#panel-resumen / #panel-estructura) es visible según la pestaña activa; la pestaña
   activa lleva subrayado y color --color-primario.

RENDER DE MARKDOWN (en app.js, sin librerías): implementa un mini-renderizador propio que
convierta el Markdown devuelto por Claude a HTML. Debe soportar: encabezados #..###### ,
listas con viñetas (-, *, +) y numeradas, bloques de código ```, código en línea `, negrita
**, cursiva *, citas >, regla horizontal (---/***/___), enlaces [txt](url) que abren en
pestaña nueva, y párrafos. ESCAPA SIEMPRE el HTML del texto (& < >) antes de procesar para
evitar inyección. Estiliza el HTML resultante con la paleta: h2 con borde inferior, h3 en
cyan, code/pre con fondo --color-superficie-2 y borde, blockquote con barra cyan, etc.

Detalles de estilo a respetar: scrollbars finos en cyan tenue; barra de progreso con pista
--color-superficie-2 y relleno con --gradiente-marca animado vía transición de width;
spinner circular que gira (borde superior cyan).

═══════════════════════════════════════════════
7. FLUJO AL SUBIR / ARRASTRAR UN PDF (con barra de progreso real)
═══════════════════════════════════════════════
Se puede subir con el botón (input file) o arrastrando el PDF a la zona central; también se
intercepta el drop en toda la ventana para que el navegador no abra el archivo. Solo se
aceptan .pdf (validar en cliente y servidor).

Backend POST /api/subir: NO responde un JSON único; responde un STREAM NDJSON (mimetype
application/x-ndjson), una línea JSON por evento, usando una respuesta generadora de Flask
(Response + stream_with_context). El PDF se guarda en disco ANTES de empezar a generar el
stream (dentro del contexto de request). Pasos y eventos que emite, en orden:
  1) Guarda el PDF y calcula identificadores:
     - nombre_base = nombre del archivo sin extensión; slug = slugify(nombre_base).
     - fecha = "AAAA-MM-DD" (hoy); sello = "AAAAMMDD-HHMMSS".
     - nombre de carpeta = "AAAA-MM-DD_<slug>"; si ya existe ese día, añade "_HHMMSS" para
       evitar colisiones. id de sesión (sid) = "<sello>_<slug>".
     - crea ./data/sesiones/<carpeta>/ y guarda ahí original.pdf.
  2) Emite {"pct":12,"label":"Extrayendo texto del PDF..."} y extrae el texto con pdfplumber.
     Si la extracción falla, borra la carpeta recién creada y emite {"error":"..."} y corta.
     Si el PDF no tiene texto (escaneado), usa un texto placeholder y continúa.
  3) Emite {"pct":38,"label":"Generando resumen con Claude..."} y llama a generar_resumen().
  4) Emite {"pct":68,"label":"Generando estructura de estudio..."} y llama a
     generar_estructura().
  5) Emite {"pct":90,"label":"Guardando sesion..."} y guarda en la carpeta de la sesión:
       - original.pdf   (ya guardado)
       - resumen.md     (texto del resumen)
       - estructura.md  (texto de la estructura)
       - meta.json      (objeto con: id, titulo=nombre_base, fecha, carpeta, y "rutas":
                         {pdf:"original.pdf", resumen:"resumen.md", estructura:"estructura.md"})
     Luego inserta al PRINCIPIO de ./data/indice.json una entrada {id, titulo, fecha, carpeta}
     (lo más reciente primero) y guarda el índice.
  6) Emite el evento final {"done":true,"pct":100,"label":"Listo", id, titulo, fecha,
     resumen, estructura, pdf_url}.

Frontend: al iniciar la subida muestra el estado #cargando con la barra al 0% y texto
"Subiendo documento...". Lee la respuesta como STREAM (fetch + response.body.getReader() +
TextDecoder), parte por saltos de línea, parsea cada línea JSON y por cada evento actualiza
la barra (ancho = pct%, muestra el porcentaje y el label). Si llega {"error"} lanza error,
vuelve al estado vacío y avisa. Cuando llega {"done":true}: pone la barra al 100%, recarga
la lista de sesiones desde /api/sesiones y pinta el documento (la barra/estado de carga
desaparece y se muestra #resultado). Resultado: barra de progreso visible con avance real
(extrayendo → resumen → estructura → guardando) que desaparece al terminar.

═══════════════════════════════════════════════
8. PERSISTENCIA (no negociable)
═══════════════════════════════════════════════
- Toda la información vive en ./data/ de este proyecto (rutas relativas).
- Al levantar el servidor lee ./data/indice.json y la barra lateral muestra todas las
  sesiones anteriores (el front las pide a /api/sesiones al cargar).
- Cerrar, reiniciar o apagar la computadora NUNCA borra una sesión.
- Al clickear una sesión vieja se abre su resumen y estructura guardados (leídos de
  resumen.md y estructura.md) SIN reprocesar el PDF.

═══════════════════════════════════════════════
9. RENOMBRAR Y ELIMINAR SESIONES
═══════════════════════════════════════════════
RENOMBRAR (botón "✎"): edición EN LÍNEA dentro de la tarjeta. Al hacer clic, el título se
reemplaza por un <input> de texto prellenado con foco y selección. Enter guarda; Escape o
blur sin cambios cancela; click en los botones de acción no debe disparar la apertura de la
sesión (stopPropagation). Al guardar, POST a /api/sesion/<sid>/renombrar con body JSON
{"titulo": nuevo}. El backend actualiza el "titulo" en ./data/indice.json Y en el meta.json
de esa sesión, para que el cambio PERSISTA tras reiniciar. El front recarga la lista y, si la
sesión renombrada está abierta, actualiza el título de la topbar.

ELIMINAR (botón "🗑"): pide confirmación ("¿Eliminar la sesion ...? Esta accion no se puede
deshacer."). Si se confirma, DELETE a /api/sesion/<sid>. El backend borra por completo la
carpeta ./data/sesiones/<carpeta>/ (con original.pdf, resumen.md, estructura.md, meta.json) y
quita la entrada de ./data/indice.json. El front recarga la lista; si la sesión borrada
estaba abierta, vuelve al estado vacío (topbar a "Selecciona o sube un documento" y se muestra
la zona de arrastre).

═══════════════════════════════════════════════
10. ENTREGA FINAL
═══════════════════════════════════════════════
- Instala las dependencias en ./venv y crea TODAS las carpetas (./data, ./data/sesiones,
  ./data/indice.json con []).
- Incluye un README.md con: descripción, que el motor es Claude Code headless (claude -p) sin
  API key, el comando EXACTO para levantar el servidor, la URL, la estructura de ./data/ y la
  tabla de endpoints.
- Verifica que el servidor arranca sin errores (lánzalo, comprueba que GET / responde 200 y
  que GET /api/sesiones devuelve []), y deja la carpeta ./data limpia (índice []).
- Al final dime EN UNA LÍNEA el comando para levantarlo y la URL para abrirlo.

Comando esperado para correr:  ./venv/bin/python app.py
URL del panel:                 http://127.0.0.1:5050