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 1Columna 2Columna 3
Dato ADato BDato C
Dato DDato EDato F
` }, { label:"🔢 Pasos", code:`
1
Primer paso

Descripción del primer paso.

2
Segundo paso

Descripción del segundo paso.

3
Tercer paso

Descripció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
❌ Incorrecto / Errores comunes
` }, { label:"🔢 Stats", code:`
85%
Descripción del dato estadístico
3x
Otro indicador importante
+40
Un tercer dato relevante
` }, { label:"🔲 Tarjetas", code:`
🔧
Elemento 1

Descripción breve

Elemento 2

Descripción breve

🎯
Elemento 3

Descripción breve

📌
Elemento 4

Descripció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(audience||"")} · ${esc(duration||"")}
Módulos
${sideItems}
Recursos
Progreso0%
${esc(title)}
${esc(title)}
${esc(audience||"")}
${modules.length} módulos
${esc(duration||"")}
${withEval?`
Con evaluaciones
`:""}
${modCards}
${modScreens} ${quizScreens}
📚 Glosario Técnico
${glossCards}