window.TeacherRoomClient = { async request(baseUrl,user,path,options={}) { const token=window.ProgressSync?.getSession(user?.id); const response=await fetch(baseUrl.replace(/\/$/,'')+path,{cache:'no-store',...options,headers:{'Content-Type':'application/json',Authorization:'Bearer '+(token||''),...(window.PlatformRelease?.epoch?{'X-TB-Progress-Epoch':window.PlatformRelease.epoch}:{}),...options.headers}}); const data=await response.json().catch(()=>{throw Error('La API no devolvió datos válidos. Pulsa Actualizar para reintentar.');}); if(!response.ok)throw Object.assign(new Error(data.error||'No se pudo conectar. Reintenta.'),{status:response.status}); return data; }, isExercise(item){return item?.source==='exams'&&item.requiresScore!==false&&!['student_specific_request','request','suggestion','contact'].includes(item.answers?.type||item.category);}, async list(baseUrl,user,{source='exams',state='',category='',offset=0}){ const target=Number(offset)+31; const scan=async(params,accept=()=>true,limit=target)=>{ const items=[],seen=new Set();let cursor=0; while(items.length{ const results=await Promise.allSettled(jobs),good=results.filter(r=>r.status==='fulfilled'),bad=results.filter(r=>r.status==='rejected'); if(!good.length)throw bad[0].reason; return {groups:results.map(r=>r.status==='fulfilled'?r.value:[]),warning:bad.length?'Parte de la bandeja no pudo cargarse. '+bad.map(r=>r.reason.message).join(' '):''}; }; let items=[],warning=''; if(source==='exams'){ // The original API treats every nonempty category as an exact answer type. items=await scan({source:'exams',state,category:category==='exercises'?'':category},item=>this.isExercise(item)); }else if(source==='messages'){ if(category==='student_specific_request')items=await scan({source:'exams',state,category},item=>!this.isExercise(item)); else if(category==='general')items=await scan({source:'messages',state,category:''}); else{ const result=await collect([scan({source:'messages',state,category:''}),scan({source:'exams',state,category:''},item=>!this.isExercise(item)&&item.answers?.type!=='contact')]); warning=result.warning; // Interleave the two independently paginated sources without hiding either. for(let i=0;ig.length));i++)for(const group of result.groups)if(group[i])items.push(group[i]); } }else if(source==='profiles'){ const result=await collect([scan({source:'profiles',state:'',category:''},()=>true,Infinity),scan({source:'exams',state:'',category:'student_specific_request'},()=>true,Infinity)]); warning=result.warning;const profiles=new Map(); for(const item of result.groups[1].slice().sort((a,b)=>String(b.created_at||'').localeCompare(String(a.created_at||'')))){ const id=String(item.user_id||item.username||'');if(!id||profiles.has(id))continue; const snapshot=item.answers?.personalizationSnapshot||{},learner=item.learner||{}; const summary={age:learner.age||snapshot.age||'',priorityInterests:learner.priorityInterests?.length?learner.priorityInterests:Array.isArray(snapshot.priorityInterests)?snapshot.priorityInterests:[],masterIds:learner.masterIds||[]}; if(summary.age||summary.priorityInterests.length)profiles.set(id,{id,user_id:id,source:'profiles',username:item.username,learner:summary}); } for(const item of result.groups[0]){ const id=String(item.user_id||item.id),previous=profiles.get(id)?.learner||{},learner=item.learner||{}; profiles.set(id,{...item,learner:{age:learner.age||previous.age||'',priorityInterests:learner.priorityInterests?.length?learner.priorityInterests:previous.priorityInterests||[],masterIds:learner.masterIds||[]}}); } items=[...profiles.values()].sort((a,b)=>String(a.username).localeCompare(String(b.username))); } return {items:items.slice(Number(offset),Number(offset)+30),hasMore:items.length>Number(offset)+30,warning}; } }; const inboxLabels={pending:'Pendiente',reviewed:'Revisado',responded:'Respondido',graded:'Calificado',profile:'Perfil guardado',suggestion:'Sugerencia',example:'Ejemplo',request:'Petición',contact:'Contacts',other:'Otro',alien_contact_group:'Alien Contact',special_technique:'Special Technique',student_specific_request:'Petición específica'}; function SubmissionContent({value,depth=0}) { if(value==null)return null; if(typeof value!=='object')return

{String(value)}

; if(depth>6)return

Contenido adicional disponible en la actividad original.

; if(Array.isArray(value))return
{value.map((item,index)=>
)}
; const labels={studentAnswer:'Respuesta del alumno',studentText:'Ejemplos del alumno',prompt:'Consigna',question:'Pregunta',displayQuestion:'Pregunta',answer:'Respuesta',response:'Respuesta',text:'Texto',category:'Categoría',description:'Descripción',requests:'Peticiones',contacts:'Contacts',displayName:'Nombre',roleCode:'Relación',lifeStatus:'Estado',languageRef:'Referencia',age:'Edad',priorityInterests:'Intereses',personalizationSnapshot:'Perfil al enviar',answers:'Respuestas',pages:'Ejemplos',title:'Título',feedback:'Comentario',correction:'Corrección',recommendations:'Recursos',score:'Calificación',teacher_explanation:'Respuesta del maestro',submittedAt:'Fecha de envío'}; Object.assign(labels,{questions:'Preguntas',codedQuestion:'Pregunta',expectedAnswer:'Ejemplo de respuesta',selectedContacts:'Contacts de la actividad',roleLabel:'Relación',topicIds:'Temas',difficulty:'Dificultad',context:'Consigna de la actividad'}); const hidden=new Set(['id','userId','user_id','exerciseId','boxId','exampleId','reviewFormat','elements','type','masterIds','createdAt','updatedAt','teacherNote','schemaVersion','privacy','questionId','baseQuestionId','contactId','answerPattern','targetStyle','teacherReviewGuide','progressEpoch','contacts','selectedContacts','selectedRoles','displayName','contactIds','alienContactContexts']); const entries=Object.entries(value).filter(([key,v])=>!hidden.has(key)&&v!=null&&v!==''&&(!value.pages||!['steps','answers'].includes(key))&&(!value.questions||key!=='steps')); return
{entries.map(([key,v])=>
{labels[key]||key.replace(/([a-z])([A-Z])/g,'$1 $2').replace(/_/g,' ')}
)}
; } function InboxSessionHelp({user,baseUrl,onConnected}) { const [password,setPassword]=React.useState(''),[error,setError]=React.useState(''),[busy,setBusy]=React.useState(false); return
{ event.preventDefault();setBusy(true);setError(''); try{ const response=await fetch(baseUrl+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:user.username,password})}); const data=await response.json(); if(!response.ok)throw Error(data.error||'No se pudo iniciar sesión.'); if(String(data.user?.id)!==String(user.id))throw Error('Esta es una vista de prueba. Cierra sesión e inicia con tu cuenta real para consultar tus envíos.'); window.ProgressSync.setSession(user.id,data.sessionToken);window.dispatchEvent(new CustomEvent('tb:session-updated'));setPassword('');onConnected(); }catch(e){setError(e.message);}finally{setBusy(false);} }}>

Confirma la sesión de {user.username} para consultar información guardada en el servidor.

{error&&

{error}

}
; } function TeacherUsers({user,baseUrl}) { const [items,setItems]=React.useState([]),[username,setUsername]=React.useState(''),[password,setPassword]=React.useState(''),[busy,setBusy]=React.useState(false),[error,setError]=React.useState(''),[notice,setNotice]=React.useState(''),[offset,setOffset]=React.useState(0),[hasMore,setHasMore]=React.useState(false); const request=(path,options)=>window.TeacherRoomClient.request(baseUrl,user,path,options); async function load(page=offset){const data=await request('/teacher-room/users?offset='+page);setItems(data.items);setHasMore(data.hasMore);} React.useEffect(()=>{let live=true;setBusy(true);setError('');request('/teacher-room/users?offset='+offset).then(data=>{if(live){setItems(data.items);setHasMore(data.hasMore);}}).catch(e=>{if(live)setError(e.message);}).finally(()=>{if(live)setBusy(false);});return()=>{live=false;};},[offset,user.id,baseUrl]); async function create(event){event.preventDefault();if(busy)return;setBusy(true);setError('');setNotice('');try{await request('/teacher-room/users',{method:'POST',body:JSON.stringify({username,password})});setPassword('');setUsername('');setNotice('Usuario agregado y autorizado.');await load();}catch(e){setError(e.message);}finally{setBusy(false);}} async function revoke(item){if(busy||!window.confirm('¿Eliminar el acceso de '+item.username+'? Se cerrarán sus sesiones y se conservará su historial.'))return;setBusy(true);setError('');setNotice('');try{await request('/teacher-room/users/'+encodeURIComponent(item.id)+'/revoke',{method:'POST',body:JSON.stringify({confirmation:item.id})});setNotice('Acceso retirado. Historial conservado.');await load();}catch(e){setError(e.message);}finally{setBusy(false);}} return

Administrar usuarios

Exclusivo de la sesión especial de p. Eliminar retira el acceso y conserva el historial.

{error&&

{error}

}{notice&&

{notice}

}
    {items.map(item=>
  • {item.username} · {item.active?'Autorizado':'Acceso retirado'}
  • )}
; } function TeacherAdministrationPanels({user,baseUrl,users=false}){ const [open,setOpen]=React.useState(''); return
{users&&
{if(e.currentTarget.open)setOpen('users');else setOpen(v=>v==='users'?'':v);}}>Administrar usuarios{open==='users'&&}
}
{if(e.currentTarget.open)setOpen('authoring');else setOpen(v=>v==='authoring'?'':v);}}>Creación de ejercicios y VOC Maker{open==='authoring'&&typeof EducationalAuthoring!=='undefined'&&}
Diseño de niveles
; } function TeacherRoomPage({user,baseUrl}) { if(['a','j','g','r'].includes(String(user?.username||'').trim().toLowerCase()))return

Crear y enviar ejercicios

Tus propuestas se envían a p para supervisión.

; const [allowed,setAllowed]=React.useState(false),[source,setSource]=React.useState('exams'),[state,setState]=React.useState('pending'),[category,setCategory]=React.useState(''); const [offset,setOffset]=React.useState(0),[items,setItems]=React.useState([]),[hasMore,setHasMore]=React.useState(false),[refresh,setRefresh]=React.useState(0),[search,setSearch]=React.useState(''); const [selected,setSelected]=React.useState(null),[feedback,setFeedback]=React.useState(''),[score,setScore]=React.useState(''),[pages,setPages]=React.useState([]),[pageIndex,setPageIndex]=React.useState(0),[resources,setResources]=React.useState(''); const [error,setError]=React.useState(null),[notice,setNotice]=React.useState(''),[loading,setLoading]=React.useState(false),[saving,setSaving]=React.useState(false),[dirty,setDirty]=React.useState(false),[progressPending,setProgressPending]=React.useState(false),[serverReviewVersion,setServerReviewVersion]=React.useState(''),[serverCredential,setServerCredential]=React.useState(''); const request=(path,options)=>window.TeacherRoomClient.request(baseUrl,user,path,options); const canLeave=()=>!dirty||window.confirm('Tienes cambios sin guardar. ¿Quieres salir de esta revisión?'); React.useEffect(()=>{const handler=e=>{if(dirty){e.preventDefault();e.returnValue='';}};window.addEventListener('beforeunload',handler);return()=>window.removeEventListener('beforeunload',handler);},[dirty]); React.useEffect(()=>{ let live=true;setError(null);setLoading(true); request('/teacher-room/access').then(data=>{if(!data.allowed)throw Object.assign(Error('Usuario no autorizado'),{status:403});if(live){setAllowed(true);setServerReviewVersion(data.reviewApiVersion||'');setServerCredential(data.credentialType||'');}return window.TeacherRoomClient.list(baseUrl,user,{source,state,category,offset});}) .then(data=>{if(live){setItems(data.items);setHasMore(data.hasMore);if(data.warning)setError(Error(data.warning));}}).catch(e=>{if(live){setError(e);if(e.status===401||e.status===403)setAllowed(false);}}).finally(()=>{if(live)setLoading(false);}); return()=>{live=false;}; },[user.id,baseUrl,source,state,category,offset,refresh]); const scorable=item=>window.TeacherRoomClient.isExercise(item); const finished=item=>item?.source==='exams'?Boolean(item.reviewed&&(!scorable(item)||item.score!=null)):item?.status==='responded'; function open(item){setProgressPending(false);setSelected(item);setFeedback(item.reply||'');setScore(item.score??'');setResources((item.recommendations?.recommendations||[]).join(', '));setError(null);setDirty(false);setPageIndex(0);setPages(window.ReviewSubmission.pages(item.answers,window.N1_C45_AI_EXAM).map(page=>({...page,...item.recommendations?.pages?.find(p=>p.exampleId===page.exampleId)})));} function navigate(nextSource,nextCategory=''){if(!canLeave())return;setProgressPending(false);setSource(nextSource);setCategory(nextCategory);setOffset(0);setState('pending');setSelected(null);setItems([]);setSearch('');setDirty(false);setNotice('');} function filter(set,value){if(!canLeave())return;set(value);setOffset(0);setSelected(null);setDirty(false);} const readonly=finished(selected),needsScore=scorable(selected),passingScore=selected?.passingScore||90; async function save(event){ event.preventDefault();if(saving)return;setSaving(true);setError(null);setNotice(''); try{ const result=await request(`/teacher-room/items/${selected.source}/${encodeURIComponent(selected.id)}/review`,{method:'POST',body:JSON.stringify({feedback,score:needsScore?Number(score):'',reviewed:true,pages,recommendations:resources.split(',').map(s=>s.trim()).filter(Boolean),version:selected.version})}); const confirmed=selected.source==='exams'?(await request(`/teacher-room/items/exams/${encodeURIComponent(selected.id)}`)).item:result.item; if(!confirmed||!finished(confirmed)||(needsScore&&Number(confirmed.score)!==Number(score)))throw Error(serverReviewVersion!=='score-save-2'?'La API conectada no tiene la actualización del guardado de notas. Publica el archivo actualizacion-api.zip nuevo en api.todosbilingues.com y vuelve a intentar. Tu nota y correcciones siguen aquí.':'La lectura de la entrega no confirmó la nota guardada. Tu nota y correcciones siguen aquí; no se marcó como calificada.');open(confirmed);setProgressPending(Boolean(result.progressPending));setDirty(false);setNotice(result.progressPending?'Calificación guardada. La sincronización de Level está pendiente.':'Guardado. Tu respuesta ya está disponible para el alumno.');setRefresh(n=>n+1); }catch(e){setError(e);}finally{setSaving(false);} } if(!allowed)return

Sala de Maestros

{loading?

Verificando acceso…

:<>
{error?.status===401||error?.status===403?'Usuario no autorizado para revisión de alumnos':error?.message||'Usuario no autorizado'}
}
; const filters=[['','Todas las actividades'],['alien_contact_group','Alien Contact'],['special_technique','Special Technique'],['alien_contact_level_t2_room','Mirror · primera parte'],['alien_contact_level_t2_you','Mirror · entrega final'],['personal_questionnaire_structure_01_01_1','Cuestionario lógico'],['final_level1','Examen final del nivel 1']]; const typeLabel=item=>filters.find(([id])=>id===(item.category||item.answers?.type))?.[1]||(item.exam_key==='L1-E48'?'Examen final':item.source==='messages'?'Mensaje':'Actividad'); const profile=learner=>
Edad
{learner?.age||'Sin registrar'}
Intereses{(learner?.priorityInterests||[]).map((label,i)=>{label})}{(learner?.masterIds||[]).length>0&&Rubros: {learner.masterIds.join(' · ')}}
; const visible=items.filter(item=>(item.username+' '+teacherExercise(item)+' '+(item.learner?.priorityInterests||[]).join(' ')).toLowerCase().includes(search.toLowerCase())); const current=pages[pageIndex]; return
Todos Bilingües

Sala de Maestros

Solo edad e intereses.
MY REAL WORLD es privado.
{source!=='profiles'&&}{source==='messages'&&}{source==='exams'&&}
{loading&&

Cargando tu bandeja…

}{error&&

{error.message}

}{notice&&

{notice}

} {source==='profiles'?
{visible.map(item=>

{item.username}

{profile(item.learner)}
)}
:
{selected?<>
{typeLabel(selected)}

Usuario que estás calificando: {selected.username}

{needsScore?'Ejercicio':'Mensaje'}: {teacherExercise(selected)}

{profile(selected.learner)}
{selected.source==='exams'&&selected.answers?.progressEpoch!==window.PlatformRelease?.epoch&&

Entrega anterior al reinicio. La revisión se conserva sin sumar progreso nuevo.

} {!pages.length?:
Ver entrega completa
}
{progressPending&&} {needsScore&&
{score===''?'Score pendiente':Number(score)>=passingScore?'✓ Aprobado':'Necesita corregir'}

{passingScore}/100 o más para aprobar.

} {current&&
Respuesta {pageIndex+1} de {pages.length}
Revisión por respuesta
CONSIGNA

{current.prompt||selected.answers?.context?.prompt||teacherExercise(selected)}

RESPUESTA DEL ALUMNO

{current.studentAnswer}

{current.expectedAnswer&&
Ver respuesta de referencia

{current.expectedAnswer}

}