{"id":22312,"date":"2025-09-30T11:03:32","date_gmt":"2025-09-30T09:03:32","guid":{"rendered":"https:\/\/www.stepv.upv.es\/?page_id=22312"},"modified":"2025-09-30T22:18:52","modified_gmt":"2025-09-30T20:18:52","slug":"testx","status":"publish","type":"page","link":"https:\/\/www.stepv.upv.es\/es\/testx\/","title":{"rendered":"TESTX"},"content":{"rendered":"<div class=\"fusion-fullwidth fullwidth-box fusion-builder-row-1 nonhundred-percent-fullwidth non-hundred-percent-height-scrolling\"  style='background-color: rgba(255,255,255,0);background-position: center center;background-repeat: no-repeat;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;'><div class=\"fusion-builder-row fusion-row \"><div  class=\"fusion-layout-column fusion_builder_column fusion_builder_column_1_1 fusion-builder-column-0 fusion-one-full fusion-column-first fusion-column-last 1_1\"  style='margin-top:0px;margin-bottom:20px;'><div class=\"fusion-column-wrapper\" style=\"padding: 0px 0px 0px 0px;background-position:left top;background-repeat:no-repeat;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;\"   data-bg-url=\"\"><meta name=\"robots\" content=\"noindex, nofollow\"><!doctype html>\n<html lang=\"es\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  <title>TESTX v0.1 - Bona sort... A X TOTES<\/title>\n  <style>\n  :root {\n    --bg:#f3f4f6;\n    --panel:#f2f2f2;\n    --accent:#22c55e;\n    --muted:#6b7280;\n    --text:#111827;\n    --wrong:#ef4444;\n    --right:#16a34a;\n  }\n\n  * { box-sizing:border-box }\n  body {\n    margin:0;\n    font-family:system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial;\n    background:linear-gradient(180deg,#f9fafb,var(--bg));\n    color:var(--text);\n  }\n\n  header {\n    position:sticky; top:0; z-index:10;\n    backdrop-filter:blur(8px);\n    background:rgba(255,255,255,.85);\n    border-bottom:1px solid rgba(0,0,0,.08);\n  }\n\n  .wrap { max-width:980px; margin:0 auto; padding:16px }\n  h1 { margin:0 0 4px; font-size:clamp(1.4rem,1.2rem + 1vw,2rem) }\n  .subtitle { margin:0; color:var(--muted); font-size:.95rem }\n\n  .controls { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; align-items:center }\n  button, select, input[type=\"checkbox\"], label {\n    background:#ffffff;\n    color:var(--text);\n    border:1px solid rgba(0,0,0,.12);\n    padding:10px 14px;\n    border-radius:12px;\n    cursor:pointer;\n    font-weight:600;\n  }\n  input[type=\"checkbox\"]{ padding:0 10px; width:auto; height:auto; accent-color:var(--accent); border:none }\n  label.chk{ border:none; padding:0 8px 0 0; cursor:pointer; }\n  button:hover { border-color:rgba(0,0,0,.3) }\n  .accent { background:var(--accent); color:#04130a; border-color:transparent }\n  .accent:hover { filter:brightness(.95) }\n\n  main { padding:24px 16px 64px }\n\n  .card {\n    background:var(--panel);\n    border:1px solid rgba(0,0,0,.08);\n    padding:18px; border-radius:16px; margin:12px 0;\n    box-shadow:0 4px 12px rgba(0,0,0,.10);\n  }\n\n  .qheader { display:flex; justify-content:space-between; align-items:baseline; gap:8px }\n  .qtitle { font-weight:700; font-size:1.05rem }\n\n  ol.options { list-style:none; padding-left:0; margin:10px 0 0; display:grid; gap:8px }\n  .opt {\n    border:1px solid rgba(0,0,0,.12);\n    border-radius:12px; padding:10px 12px;\n    display:grid; grid-template-columns:auto 1fr auto; gap:10px; align-items:center;\n  }\n  .opt input { width:18px; height:18px; accent-color:var(--accent) }\n\n  .badge { font-size:.8rem; padding:2px 8px; border-radius:999px; border:1px solid rgba(0,0,0,.15); color:var(--muted) }\n  .feedback { margin-top:8px; font-size:.95rem }\n  .right { color:var(--right) } .wrong { color:var(--wrong) }\n\n  footer {\n    position:sticky; bottom:0;\n    background:rgba(255,255,255,.9);\n    border-top:1px solid rgba(0,0,0,.08);\n    backdrop-filter:blur(8px);\n  }\n  .footer-inner {\n    max-width:980px; margin:0 auto; padding:16px;\n    display:grid; gap:10px; grid-template-columns:1fr auto; align-items:center\n  }\n  .scorebox { font-weight:700 }\n\n  progress { width:100%; height:10px; border-radius:999px; overflow:hidden }\n  progress::-webkit-progress-bar { background-color:#e5e7eb }\n  progress::-webkit-progress-value { background-color:var(--accent) }\n\n  @media (max-width:640px){ .footer-inner{ grid-template-columns:1fr } }\n\n  .hint { color:var(--muted); font-size:.85rem; margin-top:6px }\n  .filezone { border:1px dashed rgba(0,0,0,.25); border-radius:12px; padding:10px 12px; color:var(--muted) }\n  <\/style>\n<\/head>\n<body>\n  <header>\n    <div class=\"wrap\">\n      <h1>TEST STEPV - Bona sort... A PER TOTES - V0.20<\/h1>\n      <p class=\"subtitle\">Marca els blocs a carregar i prem \u201cCargar bloques\u201d.<\/p>\n\n      <div class=\"controls\">\n        <button id=\"shuffleBtn\" title=\"Barajar preguntas\">\ud83d\udd00 Barajar<\/button>\n        <button id=\"checkAllBtn\" class=\"accent\" title=\"Corregir todo\">\u2705 Corregir todo<\/button>\n        <button id=\"showAnsBtn\" title=\"Revelar soluciones\">\ud83d\udc41\ufe0f Mostrar soluciones<\/button>\n        <button id=\"resetBtn\" title=\"Reiniciar test\">\u267b\ufe0f Reiniciar<\/button>\n        <button id=\"exportResultsBtn\" title=\"Exportar resultados (XLS)\">\ud83e\uddfe Exportar resultados<\/button>\n        <select id=\"modeSel\" title=\"Modo de correcci\u00f3n\">\n          <option value=\"deferred\">Correcci\u00f3n al final<\/option>\n          <option value=\"immediate\">Correcci\u00f3n inmediata<\/option>\n        <\/select>\n      <\/div>\n\n      <!-- Selector de bloques 1\u20135 -->\n      <div class=\"controls\">\n        <label class=\"chk\"><input type=\"checkbox\" id=\"b1\" checked> Bloque 1<\/label>\n        <label class=\"chk\"><input type=\"checkbox\" id=\"b2\" checked> Bloque 2<\/label>\n        <label class=\"chk\"><input type=\"checkbox\" id=\"b3\" checked> Bloque 3<\/label>\n        <label class=\"chk\"><input type=\"checkbox\" id=\"b4\"> Bloque 4<\/label>\n        <label class=\"chk\"><input type=\"checkbox\" id=\"b5\"> Bloque 5<\/label>\n        <button id=\"loadBlocksBtn\" class=\"accent\">\ud83c\udf10 Cargar bloques<\/button>\n        <span id=\"loadStatus\" class=\"hint\"><\/span>\n      <\/div>\n\n      <!-- (Opcional) importaci\u00f3n por archivo -->\n      <div class=\"controls\" style=\"margin-top:8px;\">\n        <input type=\"file\" id=\"fileInput\" accept=\"application\/json\" \/>\n        <div id=\"dropzone\" class=\"filezone\">Arrastra y suelta aqu\u00ed un archivo JSON de preguntas para incorporarlo<\/div>\n      <\/div>\n\n      <div class=\"hint\">\n        Formato JSON esperado: [{\"q\": \"...\", \"options\": [\"A\",\"B\",\"C\",\"D\"], \"answer\": 0, \"source\": {\"pdf\":\"...\", \"page\": 1, \"paragraph\":\"...\"}}]\n      <\/div>\n    <\/div>\n  <\/header>\n\n  <main class=\"wrap\" id=\"quiz\"><\/main>\n\n  <footer>\n    <div class=\"footer-inner\">\n      <div class=\"scorebox\" id=\"scoreBox\">Aciertos: 0 \/ 0<\/div>\n      <progress id=\"progress\" value=\"0\" max=\"0\"><\/progress>\n    <\/div>\n  <\/footer>\n\n  <script>\n    \/\/ ================== Config ==================\n    const SOURCES = {\n      b1: \"https:\/\/www.stepv.upv.es\/va\/data_json_bloc01\/\",\n      b2: \"https:\/\/www.stepv.upv.es\/va\/data_json_bloc02\",\n      b3: \"https:\/\/www.stepv.upv.es\/va\/data_json_bloc03\",\n      b4: \"https:\/\/www.stepv.upv.es\/va\/data_json_bloc04\",\n      b5: \"https:\/\/www.stepv.upv.es\/va\/data_json_bloc05\"\n    };\n\n    \/\/ ================== Estado y datos ==================\n    let data = [];\n    let mode = 'deferred';\n    let state = [];\n\n    \/\/ ================== DOM ==================\n    const quiz = document.getElementById('quiz');\n    const scoreBox = document.getElementById('scoreBox');\n    const progress = document.getElementById('progress');\n    const modeSel = document.getElementById('modeSel');\n    const fileInput = document.getElementById('fileInput');\n    const dropzone = document.getElementById('dropzone');\n    const b1 = document.getElementById('b1');\n    const b2 = document.getElementById('b2');\n    const b3 = document.getElementById('b3');\n    const b4 = document.getElementById('b4');\n    const b5 = document.getElementById('b5');\n    const loadBlocksBtn = document.getElementById('loadBlocksBtn');\n    const loadStatus = document.getElementById('loadStatus');\n\n    modeSel.addEventListener('change', e => mode = e.target.value);\n\n    \/\/ ================== Render ==================\n    function render() {\n      quiz.innerHTML = '';\n      data.forEach((item, idx) => {\n        const card = document.createElement('article');\n        card.className = 'card';\n\n        const head = document.createElement('div');\n        head.className = 'qheader';\n        head.innerHTML = `\n          <div class=\"qtitle\">${idx+1}. ${item.q}<\/div>\n          <div class=\"badge\">Pregunta ${idx+1} \/ ${data.length} \u2014 ${item.blockName || ''}<\/div>\n        `;\n        card.appendChild(head);\n\n        const list = document.createElement('ol');\n        list.className = 'options';\n\n        item.options.forEach((opt, i) => {\n          const li = document.createElement('li');\n          li.className = 'opt';\n          li.innerHTML = `\n            <input type=\"radio\" name=\"q${idx}\" id=\"q${idx}o${i}\" \/>\n            <label for=\"q${idx}o${i}\">${opt}<\/label>\n            <span class=\"badge\"><\/span>\n          `;\n          const input = li.querySelector('input');\n          input.checked = state[idx] === i;\n          input.addEventListener('change', () => {\n            state[idx] = i;\n            if (mode === 'immediate') checkOne(idx, li);\n            updateScore();\n          });\n          list.appendChild(li);\n        });\n\n        const fb = document.createElement('div');\n        fb.className = 'feedback';\n        card.appendChild(list);\n        card.appendChild(fb);\n\n        const row = document.createElement('div');\n        row.className = 'controls';\n        const btnCheck = document.createElement('button');\n        btnCheck.textContent = 'Corregir esta';\n        btnCheck.addEventListener('click', () => checkOne(idx));\n        const btnReveal = document.createElement('button');\n        btnReveal.textContent = 'Mostrar soluci\u00f3n';\n        btnReveal.addEventListener('click', () => reveal(idx));\n        row.appendChild(btnCheck);\n        row.appendChild(btnReveal);\n        card.appendChild(row);\n\n        quiz.appendChild(card);\n      });\n\n      updateScore();\n      progress.max = data.length || 0;\n    }\n\n    function checkOne(idx, directLi = null) {\n      if (state[idx] === null || state[idx] === undefined) return;\n      const card = quiz.children[idx];\n      const list = card.querySelector('.options');\n      const fb = card.querySelector('.feedback');\n      const answer = data[idx].answer;\n\n      [...list.children].forEach(li => {\n        li.style.borderColor = 'rgba(0,0,0,.12)';\n        const badge = li.querySelector('.badge');\n        badge.textContent = '';\n        badge.style.color = 'inherit';\n      });\n\n      const sel = state[idx];\n      const selectedLi = directLi || list.children[sel];\n      const ref = data[idx].source || null;\n      const refText = ref ? ` Verifica en: <em>${ref.pdf}<\/em>, p\u00e1g. ${ref.page}, p\u00e1rr.\/tabla: <em>${ref.paragraph || '\u2014'}<\/em>.` : '';\n\n      if (sel === answer) {\n        selectedLi.style.borderColor = 'var(--right)';\n        selectedLi.querySelector('.badge').textContent = '\u2714\ufe0f Correcta';\n        selectedLi.querySelector('.badge').style.color = 'var(--right)';\n        fb.innerHTML = '<span class=\"right\">\u00a1Bien! Respuesta correcta.<\/span>' + refText;\n      } else {\n        selectedLi.style.borderColor = 'var(--wrong)';\n        selectedLi.querySelector('.badge').textContent = '\u2716\ufe0f Incorrecta';\n        selectedLi.querySelector('.badge').style.color = 'var(--wrong)';\n        const rightLi = list.children[answer];\n        rightLi.style.borderColor = 'var(--right)';\n        rightLi.querySelector('.badge').textContent = '\u2714\ufe0f Correcta';\n        rightLi.querySelector('.badge').style.color = 'var(--right)';\n        fb.innerHTML = `<span class=\"wrong\">No es correcto.<\/span> La correcta es: <strong>${data[idx].options[answer]}<\/strong>.` + refText;\n      }\n      updateScore();\n    }\n\n    function reveal(idx) {\n      const card = quiz.children[idx];\n      const list = card.querySelector('.options');\n      const fb = card.querySelector('.feedback');\n      const answer = data[idx].answer;\n      const ref = data[idx].source || null;\n      const refText = ref ? ` Verifica en: <em>${ref.pdf}<\/em>, p\u00e1g. ${ref.page}, p\u00e1rr.\/tabla: <em>${ref.paragraph || '\u2014'}<\/em>.` : '';\n\n      [...list.children].forEach((li, i) => {\n        li.style.borderColor = i === answer ? 'var(--right)' : 'rgba(0,0,0,.12)';\n        const badge = li.querySelector('.badge');\n        badge.textContent = i === answer ? '\u2714\ufe0f Correcta' : '';\n        badge.style.color = i === answer ? 'var(--right)' : 'inherit';\n      });\n      fb.innerHTML = `Soluci\u00f3n: <strong>${data[idx].options[answer]}<\/strong>.` + refText;\n    }\n\n    function shuffle() {\n      for (let i = data.length - 1; i > 0; i--) {\n        const j = Math.floor(Math.random() * (i + 1));\n        [data[i], data[j]] = [data[j], data[i]];\n        [state[i], state[j]] = [state[j], state[i]];\n      }\n      render();\n    }\n\n    function resetAll() { state = new Array(data.length).fill(null); render(); }\n\n    function checkAll() { for (let i = 0; i < data.length; i++) if (state[i] !== null) checkOne(i); }\n\n    function updateScore() {\n      let answered = 0, correct = 0;\n      for (let i = 0; i < data.length; i++) {\n        if (state[i] !== null && state[i] !== undefined) {\n          answered++;\n          if (state[i] === data[i].answer) correct++;\n        }\n      }\n      scoreBox.textContent = `Aciertos: ${correct} \/ ${answered}`;\n      progress.value = correct;\n      progress.max = data.length || 0;\n    }\n\n    \/\/ ================== Exportaci\u00f3n XLS (Resumen por bloque + Detalle) ==================\n    function exportResults() {\n      if (!data.length) { alert('No hay preguntas cargadas.'); return; }\n\n      \/\/ M\u00e9tricas por bloque\n      const blocks = {};\n      data.forEach((q, i) => {\n        const key = q.blockKey || 'unknown';\n        if (!blocks[key]) blocks[key] = {\n          name: q.blockName || key,\n          total: 0, answered: 0, correct: 0, wrong: 0\n        };\n        const b = blocks[key];\n        b.total++;\n        const sel = state[i];\n        if (sel !== undefined && sel !== null) {\n          b.answered++;\n          if (sel === q.answer) b.correct++; else b.wrong++;\n        }\n      });\n\n      \/\/ Escapar XML\n      const x = (s) => String(s ?? '').replace(\/&\/g,'&').replace(\/<\/g,'<').replace(\/>\/g,'>').replace(\/\"\/g,'\"');\n\n      const now = new Date().toISOString();\n      const title = (document.querySelector('h1')?.textContent?.trim() || 'Test');\n\n      const headerXML =\n`<?xml version=\"1.0\"?>\n<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"\n          xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n          xmlns:x=\"urn:schemas-microsoft-com:office:excel\"\n          xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">\n  <DocumentProperties xmlns=\"urn:schemas-microsoft-com:office:office\">\n    <Author>TESTX<\/Author>\n    <LastAuthor>TESTX<\/LastAuthor>\n    <Created>${x(now)}<\/Created>\n    <Company>TESTX<\/Company>\n    <Version>1.0<\/Version>\n  <\/DocumentProperties>\n  <ExcelWorkbook xmlns=\"urn:schemas-microsoft-com:office:excel\">\n    <ProtectStructure>False<\/ProtectStructure>\n    <ProtectWindows>False<\/ProtectWindows>\n  <\/ExcelWorkbook>`;\n\n      \/\/ Hoja 1: Resumen\n      let sheetResumen =\n`  <Worksheet ss:Name=\"Resumen\">\n    <Table>\n      <Row>\n        <Cell><Data ss:Type=\"String\">Bloque<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">Total<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">Respondidas<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">Aciertos<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">Fallos<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">% Acierto<\/Data><\/Cell>\n      <\/Row>`;\n\n      Object.values(blocks).forEach(b => {\n        const acc = b.answered ? (b.correct \/ b.answered) : 0;\n        sheetResumen += `\n      <Row>\n        <Cell><Data ss:Type=\"String\">${x(b.name)}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${b.total}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${b.answered}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${b.correct}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${b.wrong}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${(acc*100).toFixed(1)}%<\/Data><\/Cell>\n      <\/Row>`;\n      });\n\n      const totals = Object.values(blocks).reduce((a,b)=>({\n        total:a.total+b.total, answered:a.answered+b.answered, correct:a.correct+b.correct, wrong:a.wrong+b.wrong\n      }), {total:0,answered:0,correct:0,wrong:0});\n      const accGlobal = totals.answered ? (totals.correct\/totals.answered) : 0;\n\n      sheetResumen += `\n      <Row>\n        <Cell><Data ss:Type=\"String\">TOTAL<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${totals.total}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${totals.answered}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${totals.correct}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${totals.wrong}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${(accGlobal*100).toFixed(1)}%<\/Data><\/Cell>\n      <\/Row>\n    <\/Table>\n  <\/Worksheet>`;\n\n      \/\/ Hoja 2: Detalle\n      let sheetDetalle =\n`  <Worksheet ss:Name=\"Detalle\">\n    <Table>\n      <Row>\n        <Cell><Data ss:Type=\"String\">Bloque<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">#<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">Pregunta<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">Marcada<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">Correcta<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">\u00bfAcierto?<\/Data><\/Cell>\n      <\/Row>`;\n\n      data.forEach((q, i) => {\n        const sel = state[i];\n        const selText = (sel !== undefined && sel !== null) ? q.options[sel] : '';\n        const okText  = q.options[q.answer];\n        const isOk    = (sel !== undefined && sel !== null) ? (sel === q.answer ? 'S\u00ed' : 'No') : '';\n        sheetDetalle += `\n      <Row>\n        <Cell><Data ss:Type=\"String\">${x(q.blockName || '')}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"Number\">${i+1}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${x(q.q)}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${x(selText)}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${x(okText)}<\/Data><\/Cell>\n        <Cell><Data ss:Type=\"String\">${x(isOk)}<\/Data><\/Cell>\n      <\/Row>`;\n      });\n\n      sheetDetalle += `\n    <\/Table>\n  <\/Worksheet>`;\n\n      const footerXML = `<\/Workbook>`;\n      const xml = headerXML + '\\n' + sheetResumen + '\\n' + sheetDetalle + '\\n' + footerXML;\n\n      const blob = new Blob([xml], { type: 'application\/vnd.ms-excel' });\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement('a');\n      const stamp = new Date().toISOString().replace(\/[:.]\/g,'-');\n      a.href = url; a.download = `${title.replace(\/\\s+\/g,'_')}-resultados-${stamp}.xls`;\n      document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); a.remove();\n    }\n\n    \/\/ ================== Utilidades de carga (JSON puro o incrustado en HTML) ==================\n    const htmlDecode = (s) => { const d = document.createElement(\"textarea\"); d.innerHTML = s; return d.value; };\n    const tryParseJSON = (t) => { try { return JSON.parse(t); } catch (_) { return null; } };\n\n    function findJSONInDoc(doc) {\n      const s = doc.querySelector('script[type=\"application\/json\"]');\n      if (s?.textContent?.trim()) return s.textContent.trim();\n      const candidates = [doc.querySelector('#my-json'), ...doc.querySelectorAll('pre, code')].filter(Boolean);\n      for (const el of candidates) {\n        const txt = el.textContent?.trim() || \"\";\n        if (txt.includes('{') || txt.includes('[')) return txt;\n      }\n      return doc.body?.innerHTML || \"\";\n    }\n\n    function extractJSONHeuristic(text) {\n      let parsed = tryParseJSON(text);\n      if (parsed) return parsed;\n      const des = htmlDecode(text).trim();\n      parsed = tryParseJSON(des);\n      if (parsed) return parsed;\n\n      const iObj = des.indexOf(\"{\");\n      const iArr = des.indexOf(\"[\");\n      const i = (iObj === -1) ? iArr : (iArr === -1 ? iObj : Math.min(iObj, iArr));\n      if (i >= 0) {\n        const open = des[i] === \"{\" ? \"{\" : \"[\";\n        const close = open === \"{\" ? \"}\" : \"]\";\n        let depth = 0, end = -1;\n        for (let j = i; j < des.length; j++) {\n          const ch = des[j];\n          if (ch === open) depth++;\n          else if (ch === close) { depth--; if (depth === 0) { end = j + 1; break; } }\n        }\n        if (end > i) {\n          const frag = des.slice(i, end);\n          parsed = tryParseJSON(frag);\n          if (parsed) return parsed;\n        }\n      }\n      throw new Error(\"No se ha podido extraer JSON v\u00e1lido de la p\u00e1gina.\");\n    }\n\n    function normalizeQuestions(arr, originKey) {\n      if (!Array.isArray(arr)) throw new Error('El contenido obtenido no es un array de preguntas');\n      const blockNameMap = { b1:'Bloque 1', b2:'Bloque 2', b3:'Bloque 3', b4:'Bloque 4', b5:'Bloque 5' };\n      return arr.map((p, i) => ({\n        q: String(p.q || `Pregunta importada ${i+1}`),\n        options: Array.isArray(p.options) ? p.options.slice(0,4).map(String) : ['A','B','C','D'],\n        answer: Number.isInteger(p.answer) ? p.answer : 0,\n        source: p.source && typeof p.source === 'object' ? p.source : null,\n        blockKey: originKey,\n        blockName: blockNameMap[originKey] || originKey || 'Bloque'\n      }));\n    }\n\n    async function fetchFromUrl(url, originKey) {\n      const bust = (url.includes('?') ? '&' : '?') + 'v=' + Date.now();\n      const res = await fetch(url + bust, { credentials: 'omit', cache: 'no-store' });\n      if (!res.ok) throw new Error(`HTTP ${res.status}`);\n\n      \/\/ intento JSON directo\n      try {\n        const asJson = await res.clone().json();\n        return normalizeQuestions(asJson, originKey);\n      } catch (_) {}\n\n      \/\/ parsear HTML con JSON incrustado\n      const html = await res.text();\n      const doc  = new DOMParser().parseFromString(html, 'text\/html');\n      const raw  = findJSONInDoc(doc);\n      const parsed = extractJSONHeuristic(raw);\n      return normalizeQuestions(parsed, originKey);\n    }\n\n    async function loadSelectedBlocks() {\n      const selected = [\n        b1?.checked ? 'b1' : null,\n        b2?.checked ? 'b2' : null,\n        b3?.checked ? 'b3' : null,\n        b4?.checked ? 'b4' : null,\n        b5?.checked ? 'b5' : null,\n      ].filter(Boolean);\n\n      if (!selected.length) {\n        loadStatus.textContent = 'Selecciona al menos un bloque.';\n        return;\n      }\n\n      loadBlocksBtn.disabled = true;\n      loadStatus.textContent = 'Cargando bloques...';\n      try {\n        const results = await Promise.all(selected.map(key => fetchFromUrl(SOURCES[key], key)));\n        data = results.flat();\n        state = new Array(data.length).fill(null);\n        render();\n        loadStatus.textContent = `Cargados ${data.length} \u00edtems de ${selected.length} bloque(s).`;\n      } catch (e) {\n        console.error(e);\n        loadStatus.textContent = 'Error al cargar: ' + e.message + ' (\u00bfCORS o red?)';\n      } finally {\n        loadBlocksBtn.disabled = false;\n      }\n    }\n\n    \/\/ ================== Importaci\u00f3n por archivo (opcional) ==================\n    async function handleFiles(files) {\n      for (const file of files) {\n        try {\n          const text = await file.text();\n          const incoming = JSON.parse(text);\n          const cleaned = normalizeQuestions(incoming, 'archivo');\n          data = data.concat(cleaned);\n        } catch (err) { alert('Error al leer JSON: ' + err.message); }\n      }\n      state = new Array(data.length).fill(null);\n      render();\n    }\n\n    \/\/ ================== Listeners UI ==================\n    document.getElementById('shuffleBtn').addEventListener('click', shuffle);\n    document.getElementById('checkAllBtn').addEventListener('click', checkAll);\n    document.getElementById('showAnsBtn').addEventListener('click', () => data.forEach((_, i) => reveal(i)));\n    document.getElementById('resetBtn').addEventListener('click', resetAll);\n    document.getElementById('exportResultsBtn').addEventListener('click', exportResults);\n    document.getElementById('loadBlocksBtn').addEventListener('click', loadSelectedBlocks);\n\n    if (fileInput && dropzone) {\n      fileInput.addEventListener('change', (e) => handleFiles(e.target.files));\n      ;['dragenter','dragover'].forEach(ev => dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.style.borderColor='var(--accent)'; }));\n      ;['dragleave','drop'].forEach(ev => dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.style.borderColor='rgba(0,0,0,.25)'; }));\n      dropzone.addEventListener('drop', e => handleFiles(e.dataTransfer.files));\n    }\n\n    \/\/ ================== Init ==================\n    render();           \/\/ pinta vac\u00edo\n    loadSelectedBlocks(); \/\/ carga autom\u00e1tica inicial seg\u00fan checkboxes\n  <\/script>\n<\/body>\n<\/html>\n<div class=\"fusion-clearfix\"><\/div><\/div><\/div><\/div><\/div><style type=\"text\/css\">.fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link) , .fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link):before, .fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link):after {color: #af2020;}.fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link):hover, .fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link):hover:before, .fusion-fullwidth.fusion-builder-row-1 a:not(.fusion-button):not(.fusion-builder-module-control):not(.fusion-social-network-icon):not(.fb-icon-element):not(.fusion-countdown-link):not(.fusion-rollover-link):not(.fusion-rollover-gallery):not(.fusion-button-bar):not(.add_to_cart_button):not(.show_details_button):not(.product_type_external):not(.fusion-quick-view):not(.fusion-rollover-title-link):not(.fusion-breadcrumb-link):hover:after {color: #af2020;}.fusion-fullwidth.fusion-builder-row-1 .pagination a.inactive:hover, .fusion-fullwidth.fusion-builder-row-1 .fusion-filters .fusion-filter.fusion-active a {border-color: #af2020;}.fusion-fullwidth.fusion-builder-row-1 .pagination .current {border-color: #af2020; background-color: #af2020;}.fusion-fullwidth.fusion-builder-row-1 .fusion-filters .fusion-filter.fusion-active a, .fusion-fullwidth.fusion-builder-row-1 .fusion-date-and-formats .fusion-format-box, .fusion-fullwidth.fusion-builder-row-1 .fusion-popover, .fusion-fullwidth.fusion-builder-row-1 .tooltip-shortcode {color: #af2020;}#main .fusion-fullwidth.fusion-builder-row-1 .post .blog-shortcode-post-title a:hover {color: #af2020;}<\/style>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":29775,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-22312","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/pages\/22312","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/users\/29775"}],"replies":[{"embeddable":true,"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/comments?post=22312"}],"version-history":[{"count":0,"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/pages\/22312\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.stepv.upv.es\/es\/wp-json\/wp\/v2\/media?parent=22312"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}