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-permissionsPaso 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