async function validateReactionClip(file){
if(!file)return;
if(!/\.(gif|mp4)$/i.test(file.name))throw Error('Reaction usa GIF animado o MP4.');
if(/\.mp4$/i.test(file.name)){const url=URL.createObjectURL(file);try{await new Promise((resolve,reject)=>{const video=document.createElement('video');video.preload='metadata';video.onloadedmetadata=()=>resolve();video.onerror=()=>reject(Error('No se pudo leer el MP4.'));video.src=url;});}finally{URL.revokeObjectURL(url);}}
}
function AuthoringMediaGuide({activity,layout}){
const recommendations={reaction:layout==='embedded'?'Tarjeta completa: PNG o JPG, vertical 9:16; recomendado 900 x 1600 px. Incluye pregunta y opciones.':'Material central: GIF animado (preferido) o MP4 de cualquier duracion. Se reproduce en loop. Recomendamos clips de unos 3 segundos. Recomendado 1600 x 900 px (16:9); la tarjeta es vertical 900 x 1600 px (9:16). Exporta el GIF con repeticion infinita.',zero:'Imagen opcional: PNG, JPG, WebP o GIF; recomendado 1600 x 900 px (16:9). Video: MP4 o WebM, 1280 x 720 px (16:9). Escribe la pregunta y opciones en el formulario.',vocabulary:'Sube solamente el GIF central, sin título, palabra ni botones. Recomendado 640 x 640 px (1:1). Declara palabra, categoría y, para verbos, las formas 1, 2 y 3.',alien:'Escena de Alien Contact (ELT): GIF, PNG, JPG o WebP; recomendado 1600 x 900 px (16:9). Video: MP4 o WebM, 1280 x 720 px (16:9).',mirror:'Imagen: PNG, JPG, WebP o GIF; recomendado 1600 x 900 px (16:9). Video: MP4 o WebM, 1280 x 720 px (16:9).'};
return ;
}
function TopicAdministration({catalog,request,onChanged}){
const [id,setId]=React.useState(''),[name,setName]=React.useState(''),[version,setVersion]=React.useState(0),[category,setCategory]=React.useState('completeIdea'),[error,setError]=React.useState('');
return
Topics y subtopics
t7 agrupa t7.1, t7.2 y t7.3. Puedes cambiar el topic principal de cada ejercicio desde Ver / editar. Cambiar su clasificación no borra el ejercicio.
{exercise.question||exercise.word||(video?'Reproduce el video para revisar la pregunta y las respuestas incluidas.':compact?'Este registro no incluye una pregunta de texto.':'Escribe la pregunta o consigna: aparecerá aquí tal cual.')}
}
{!compact&&Se actualiza mientras escribes. El verde identifica la respuesta correcta o válida.}
;
}
function EducationalAuthoring({user,baseUrl}){
const [catalog,setCatalog]=React.useState(null),[items,setItems]=React.useState([]),[tab,setTab]=React.useState('create'),[error,setError]=React.useState(''),[notice,setNotice]=React.useState(''),[busy,setBusy]=React.useState(false),[selected,setSelected]=React.useState(null),[search,setSearch]=React.useState(''),[libraryType,setLibraryType]=React.useState(''),[mediaType,setMediaType]=React.useState('reaction');
const empty=type=>({type,title:'',primaryTopic:'',connectedTopics:[],difficulty:'f',contexts:type==='alien'?['elt']:['pap','elt'],layout:'generated',hotspots:[],cardMedia:'',distribution:'GENERAL',masterIds:[],question:'',options:['',''],correct:0,media:'',validAnswers:[],ideaKind:'affirmative',contactMode:'all',contactGroups:[],contactCategoryIds:[],contactRoleCodes:[],learning:{structure:.35,logic:.05},zeroMode:'normal',category:'',word:'',variants:[],unlock:'',finalWords:[]});
const [form,setForm]=React.useState(()=>empty('reaction'));
const request=(route,options)=>window.TeacherRoomClient.request(baseUrl,user,route,options);
const resolveMedia=src=>String(src||'').startsWith('/authoring/')?baseUrl.replace(/\/$/,'')+src:src;
async function upload(file,activity=form.type,purpose="materiales"){if(!file)return;if(file.size>20*1024*1024)throw Error('El archivo supera 20 MB. Reduce su tamaño.');const result=await request('/authoring/media?activity='+activity+'&purpose='+purpose,{method:'POST',headers:{'Content-Type':'application/octet-stream','X-File-Name':encodeURIComponent(file.name||'reaction-tarjeta.png')},body:file});const c=await request('/authoring/catalog');setCatalog(c);return result.src;}
const update=(key,value)=>{setForm(prev=>({...prev,[key]:value}));setNotice('');};
async function reload(){const data=await request('/authoring/content');setItems(data.items);}
React.useEffect(()=>{let live=true;Promise.all([request('/authoring/catalog'),request('/authoring/content')]).then(([c,data])=>{if(live){setCatalog(c);setItems(data.items);}}).catch(e=>{if(live)setError(e.message);});return()=>{live=false;};},[user.id,baseUrl]);
async function save(event){event.preventDefault();if(busy)return;setBusy(true);setError('');setNotice('');try{const payload={...form};if(payload.type==='vocabulary'){payload.title=payload.word;if(payload.category==='activity')payload.forms={a:{word:payload.forms?.a?.word||payload.word},b:{word:payload.forms?.b?.word||''},c:{word:payload.forms?.c?.word||''}};}if(payload.type==='zero'&&payload.zeroMode==='contact'){payload.contacts={person:{categoryIds:payload.contactCategoryIds.length?payload.contactCategoryIds:['c1'],roleCodes:payload.contactRoleCodes||[],mode:payload.contactMode==='all'?'only':payload.contactMode}};payload.question=payload.question.replaceAll('____','{{contact:person}}');payload.options=payload.options.map(s=>s.replaceAll('____','{{contact:person}}'));}for(const key of ['validAnswers','variants','finalWords'])payload[key]=payload[key].map(s=>s.trim()).filter(Boolean);if(payload.type==='reaction'){if(payload.layout==='generated'){const card=await window.SpeedCard.generate(payload,resolveMedia);payload.cardMedia=await upload(card.blob,'reaction','terminados');payload.hotspots=card.hotspots;}else{const response=await fetch(resolveMedia(payload.media));if(!response.ok)throw Error('No se pudo copiar la tarjeta terminada.');payload.cardMedia=await upload(await response.blob(),'reaction','terminados');}}const result=await request('/authoring/content',{method:'POST',body:JSON.stringify({id:selected?.id,version:selected?.version,payload})});setSelected({...result,payload});setForm(payload);setNotice('Borrador guardado en el servidor.');await reload();}catch(e){setError(e.message);}finally{setBusy(false);}}
async function state(item,next){if(busy)return;let note='';if(['corrections','rejected'].includes(next)){note=window.prompt('Explicación para el autor:');if(note===null)return;}if(next==='archived'&&!window.confirm('¿Archivar este contenido conservando sus versiones?'))return;setBusy(true);setError('');try{await request('/authoring/content/'+encodeURIComponent(item.id)+'/state',{method:'POST',body:JSON.stringify({version:item.version,state:next,note})});await reload();setNotice(next==='pending'?'Propuesta enviada a p para supervisión.':'Estado actualizado.');}catch(e){setError(e.message);}finally{setBusy(false);}}
function open(item){setSelected(item);setForm({...empty(item.payload.type),...item.payload});setTab(item.payload.type==='vocabulary'?'vocabulary':'create');setNotice('');}
const lines=value=>value.split('\n');
const multi=(name,values,options)=> ;
if(!catalog)return
}{['draft','corrections'].includes(item.state)&&}{catalog.admin&&['pending','draft'].includes(item.state)&&<>>}{catalog.admin&&})}Catálogo estático existente — requiere migración para editar
{catalog.legacyReaction.length} tokens Reaction registrados. El contenido de los videos no se puede convertir automáticamente en preguntas y opciones editables.
{catalog.legacyVocabulary.length} entradas VOC existentes; se conservan sin alteraciones.
}
{tab==='media'&&
Elige la actividad antes de subir: s- Reaction, v- vocabulario, a- Alien Contact (ELT), z- Zero Error y m- Mirror. El prefijo se agrega automaticamente; no necesitas renombrar tu archivo. Material fuente, separado por actividad. Los ejercicios terminados se consultan en Ejercicios en función o Mis propuestas.