import { useState, useCallback, useEffect, useRef, useMemo } from "react";
// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────
const COLORS = [
{ label:"Amber · Seguridad", val:"#F59E0B" },
{ label:"Sky · Tecnología", val:"#0EA5E9" },
{ label:"Emerald · Salud", val:"#10B981" },
{ label:"Red · Emergencias", val:"#EF4444" },
{ label:"Violet · Negocios", val:"#8B5CF6" },
{ label:"Orange · Construcción", val:"#F97316" },
];
const QUICK_INSERTS = [
{ label:"💡 Tip", code:`
💡 Consejo: Escribe tu consejo aquí.
` },
{ label:"⚠️ Atención", code:`⚠️ Atención: Escribe tu advertencia aquí.
` },
{ label:"🚫 Peligro", code:`🚫 Peligro: Describe el riesgo aquí.
` },
{ label:"📋 Norma", code:`📋 Normativa: Texto de la norma o regulación.
` },
{ label:"ℹ️ Info", code:`ℹ️ Dato importante: Escribe la información aquí.
` },
{ label:"📊 Tabla", code:`| Columna 1 | Columna 2 | Columna 3 |
|---|
| Dato A | Dato B | Dato C |
| Dato D | Dato E | Dato F |
` },
{ label:"🔢 Pasos", code:`1
Primer pasoDescripción del primer paso.
2
Segundo pasoDescripción del segundo paso.
3
Tercer pasoDescripción del tercer paso.
` },
{ label:"🔽 Acordeón", code:`▶ Pregunta o título expandible
Contenido que se despliega al hacer click. Puedes poner texto, listas o cualquier HTML aquí.
▶ Segundo ítem del acordeón
Más contenido oculto.
` },
{ label:"↔️ Comparar", code:`✅ Correcto / Buenas prácticas
- Práctica correcta 1
- Práctica correcta 2
❌ Incorrecto / Errores comunes
- Error común 1
- Error común 2
` },
{ label:"🔢 Stats", code:`85%
Descripción del dato estadístico
3x
Otro indicador importante
+40
Un tercer dato relevante
` },
{ label:"🔲 Tarjetas", code:`🔧
Elemento 1Descripción breve
⚡
Elemento 2Descripción breve
🎯
Elemento 3Descripción breve
📌
Elemento 4Descripción breve
` },
{ label:"💬 Cita", code:`Escribe aquí la cita, norma o referencia legal que quieres destacar. — Fuente o autor
` },
{ label:"📦 Concepto", code:`Título del concepto
Explicación detallada del concepto. Puedes escribir varios párrafos aquí con toda la información necesaria.
` },
{ label:"🖼️ Imagen", code:`🖼️
Reemplaza este bloque con una imagen real
Edita el HTML y usa: <img src="data:..." style="max-width:100%;border-radius:8px">
` },
{ label:"🎮 Botón", code:`` },
];
const SCORM_API = `var API=null;
function findAPI(w){var t=0;while(!w.API&&w.parent&&w.parent!==w&&t++<10)w=w.parent;return w.API||null;}
window.onload=function(){API=findAPI(window);if(API){API.LMSInitialize("");API.LMSSetValue("cmi.core.lesson_status","incomplete");}};
window.onunload=function(){if(API){API.LMSCommit("");API.LMSFinish("");}};`;
const CSS_REFERENCE = `CLASES CSS DISPONIBLES:
concept/concept accent, steps/step/step-n/step-body, callout tip/warning/danger/info/law,
icon-grid/icon-item/icon-circle, data-table, hl/hl danger/hl success, section-title,
accordion/acc-item/acc-head(onclick="toggleAcc(this)")/acc-body, compare/compare-col good/bad,
blockquote.quote, icon-list/li-icon, stat-row/stat/stat-n/stat-l, img-placeholder, btn-inline`;
// ─────────────────────────────────────────────────────────────────────────────
// UTILS
// ─────────────────────────────────────────────────────────────────────────────
function esc(s){ return String(s||"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); }
function hexRgb(h){ return [parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)].join(","); }
async function extractPPTX(file) {
const JSZip = window.JSZip;
if (!JSZip) throw new Error("JSZip no disponible");
const zip = await JSZip.loadAsync(file);
const keys = Object.keys(zip.files).filter(k=>/^ppt\/slides\/slide\d+\.xml$/.test(k))
.sort((a,b)=>parseInt(a.match(/\d+/)[0])-parseInt(b.match(/\d+/)[0]));
const slides = [];
for (const k of keys) {
const xml = await zip.files[k].async("string");
const texts=[], rx=/]*)?>([^<]+)<\/a:t>/g; let m;
while((m=rx.exec(xml))!==null){ const t=m[1].trim(); if(t)texts.push(t); }
if(texts.length) slides.push(texts.join(" | "));
}
return slides;
}
async function readB64(file) {
return new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(r.result.split(",")[1]); r.onerror=rej; r.readAsDataURL(file); });
}
async function extractPDF(file) {
if (!window.pdfjsLib) {
await new Promise((res, rej) => {
const sc = document.createElement("script");
sc.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
sc.onload = res; sc.onerror = rej;
document.head.appendChild(sc);
});
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
}
const ab = await file.arrayBuffer();
const pdf = await window.pdfjsLib.getDocument({ data: ab }).promise;
const pages = [];
for (let i = 1; i <= Math.min(pdf.numPages, 60); i++) {
const page = await pdf.getPage(i);
const tc = await page.getTextContent();
const text = tc.items.map(it => it.str).join(" ").trim();
if (text) pages.push("[Pagina " + i + "]: " + text);
}
return pages;
}
// ─────────────────────────────────────────────────────────────────────────────
// CLAUDE API
// ─────────────────────────────────────────────────────────────────────────────
async function callClaude(messages, sys, maxTok=1000) {
// Proxy does not accept `system` param — prepend as context in first user message
const msgsWithSys = sys
? [{ role: "user", content: "INSTRUCCIONES DEL SISTEMA:\n" + sys + "\n\n---\nAhora procede con la tarea:" },
{ role: "assistant", content: "Entendido. Procedo." },
...messages]
: messages;
let r, d;
try {
r = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: maxTok,
messages: msgsWithSys,
}),
});
} catch(netErr) {
throw new Error("Sin conexion a internet: " + netErr.message);
}
try { d = await r.json(); } catch(_) {
throw new Error("HTTP " + r.status + " — respuesta invalida del servidor.");
}
if (!r.ok || d.error) {
const msg = (d.error && (d.error.message || JSON.stringify(d.error))) || ("Error HTTP " + r.status);
const type = (d.error && d.error.type) || "";
if (type === "overloaded_error") throw new Error("API saturada. Espera e intenta de nuevo.");
if (r.status === 401) throw new Error("API key invalida (401).");
if (r.status === 429) throw new Error("Demasiadas solicitudes (429). Espera un momento.");
throw new Error(msg);
}
if (!d.content || !d.content.length) throw new Error("Respuesta vacia. Intenta de nuevo.");
return d.content.filter(function(b){ return b.type === "text"; }).map(function(b){ return b.text; }).join("");
}
function safeJSON(raw) {
if(!raw) throw new Error("Respuesta vacía");
let s = raw.replace(/^```[\w]*\s*/m,"").replace(/\s*```\s*$/m,"").trim();
const st=s.indexOf("{"), en=s.lastIndexOf("}");
if(st===-1||en===-1) throw new Error("No hay JSON en la respuesta");
s=s.slice(st,en+1);
try{ return JSON.parse(s); }catch(_){}
s=s.replace(/,(\s*[}\]])/g,"$1");
s=s.replace(/"((?:[^"\\]|\\.)*)"/g,m=>m.replace(/\n/g,"\\n").replace(/\r/g,"").replace(/\t/g," "));
try{ return JSON.parse(s); }catch(_){}
const title=(s.match(/"title"\s*:\s*"([^"]+)"/))||[,"Sin título"];
const modNames=[...s.matchAll(/"name"\s*:\s*"([^"]+)"/g)].map(m=>m[1]).slice(0,8);
const modDescs=[...s.matchAll(/"desc"\s*:\s*"([^"]+)"/g)].map(m=>m[1]);
if(modNames.length===0) throw new Error("No se pudo extraer la estructura. Intenta de nuevo.");
const modules=modNames.map((name,i)=>({id:i+1,name,desc:modDescs[i]||name}));
const terms=[...s.matchAll(/"term"\s*:\s*"([^"]+)"/g)].map(m=>m[1]);
const defs=[...s.matchAll(/"def"\s*:\s*"([^"]+)"/g)].map(m=>m[1]);
return { title:title[1], audience:"General", duration:"4 horas", modules, glossary:terms.map((t,i)=>({term:t,def:defs[i]||t})), quizzes:[] };
}
// ─────────────────────────────────────────────────────────────────────────────
// PASS 1 — Outline
// ─────────────────────────────────────────────────────────────────────────────
async function getOutline(file, courseName) {
const isPDF = file.name.endsWith(".pdf");
let messages;
if(isPDF){
const pages = await extractPDF(file);
const text = pages.join("\n").slice(0, 12000);
messages=[{role:"user",content:"Extrae estructura del curso" + (courseName ? ` \"${courseName}\"` : "") + ". Devuelve SOLO JSON.\n\nCONTENIDO:\n" + text}];
} else {
const slides=await extractPPTX(file);
messages=[{role:"user",content:"Extrae estructura del curso" + (courseName ? ` \"${courseName}\"` : "") + ". Devuelve SOLO JSON:\n\n" + slides.map((s,i)=>"[" + (i+1) + "]: " + s).join("\n").slice(0,12000)}];
}
const sys=`Extrae estructura de curso e-learning. Devuelve ÚNICAMENTE JSON empezando con {. Sin markdown. Sin apostrofes ni comillas dentro de strings.
{"title":"Titulo","audience":"Publico objetivo","duration":"X horas","modules":[{"id":1,"name":"Nombre","desc":"1 linea"}],"glossary":[{"term":"Termino","def":"Definicion"}],"quizzes":[{"mod":1,"qs":[{"q":"Pregunta?","opts":["A","B","C","D"],"c":0}]}]}
4-7 módulos, 6-10 glosario, 3 preguntas por módulo. Todo en español.`;
const raw=await callClaude(messages,sys);
return safeJSON(raw);
}
// ─────────────────────────────────────────────────────────────────────────────
// PASS 2 — Rich HTML per module
// ─────────────────────────────────────────────────────────────────────────────
async function generateModuleHTML(file, outline, mod, pptxSlides) {
const otherMods = outline.modules.filter(m=>m.id!==mod.id).map(m=>m.name).join(", ");
const prompt = `Genera el HTML COMPLETO y RICO para el MÓDULO ${mod.id}: "${mod.name}" del curso "${outline.title}".
Público: ${outline.audience}. Descripción: ${mod.desc}. Otros módulos (NO los desarrolles): ${otherMods}.
REQUISITOS:
- Usa el contenido REAL del documento fuente
- Mínimo 500 palabras específicas del tema
- Usa MUCHOS componentes distintos: callouts, steps, tables, icon-grid, accordion, compare, stats, quotes, etc.
- Devuelve SOLO HTML puro, sin html/head/body/style/script. Empieza directo con el contenido.`;
const docCtx = (pptxSlides||[]).map((s,i)=>"[" + (i+1) + "]: " + s).join("\n").slice(0,10000);
const messages=[{role:"user",content:"DOCUMENTO:\n" + docCtx + "\n\n---\n" + prompt}];
const sys=`Eres experto en diseño instruccional e-learning. Generas HTML rico para cursos SCORM.
${CSS_REFERENCE}
Devuelve ÚNICAMENTE HTML del contenido usando las clases indicadas. Contenido real, específico y abundante.`;
const raw = await callClaude(messages,sys);
return raw.replace(/^```[\w]*\s*/m,"").replace(/\s*```\s*$/m,"").trim();
}
// ─────────────────────────────────────────────────────────────────────────────
// AI EDIT — Modify existing module HTML
// ─────────────────────────────────────────────────────────────────────────────
async function aiEditModule(currentHTML, instruction, moduleName, courseTitle) {
const sys=`Eres experto en diseño instruccional e-learning. Editas HTML de módulos SCORM.
${CSS_REFERENCE}
REGLAS CRÍTICAS:
1. Devuelve ÚNICAMENTE el HTML del contenido del módulo. SIN etiquetas html/head/body.
2. Puedes (y debes) usar
${esc(title)}
${esc(audience||"")} · ${esc(duration||"")}
Módulos
${sideItems}
Recursos
${esc(title)}
${esc(title)}
${esc(audience||"")}
${modCards}
${modScreens}
${quizScreens}