Workshop Agenda Builder

Turn a duration and goal into a tight, facilitation-ready agenda—complete with timings, activities, and instructions.

Quick start — 1) Pick a preset 2) Write your outcome 3) Click Generate agenda.
What must participants leave with? e.g., 3 decisions, a draft plan, or a practiced skill.
How much time should be hands-on vs. content?
Where should the session peak—start, balanced, or toward the end?
For sessions ≥ 60 minutes, plan a 5–10 minute break roughly every hour.
Pick what belongs in the flow. You can still edit after generation.
You can edit each block after generation. The total minutes must match your duration—watch the counter.

Agenda

Total: 0 / 60 mins
    `; const sel=li.querySelector('.b-type'); if(sel) sel.value=type; attachBlockHandlers(li); return li; } function attachBlockHandlers(li){ const row = li.querySelector('.row'); const edit = li.querySelector('.edit'); const timeSpan = li.querySelector('.time'); const typeSpan = li.querySelector('.type'); const titleSpan = li.querySelector('.title'); const minsInp = li.querySelector('.b-mins'); const typeSel = li.querySelector('.b-type'); const titleInp = li.querySelector('.b-title'); const insTa = li.querySelector('.b-ins'); const btnEdit = li.querySelector('.b-edit'); const btnUp = li.querySelector('.b-up'); const btnDown = li.querySelector('.b-down'); const btnDup = li.querySelector('.b-dup'); const btnDel = li.querySelector('.b-del'); on(row,'click', ()=>{ if(edit) edit.style.display='grid'; }); on(btnEdit,'click', ()=>{ if(edit) edit.style.display='none'; update(); }); [minsInp,typeSel,titleInp,insTa].forEach(el=> on(el,'input', update)); on(btnUp,'click', ()=>{ const prev = li.previousElementSibling; if(prev && els.agenda){ els.agenda.insertBefore(li, prev); renumber(); save(); } }); on(btnDown,'click', ()=>{ const next = li.nextElementSibling; if(next && els.agenda){ els.agenda.insertBefore(next, li); renumber(); save(); } }); on(btnDup,'click', ()=>{ const clone = mkBlock(minsInp.value, typeSel.value, titleInp.value, insTa.value); if(els.agenda) els.agenda.insertBefore(clone, li.nextElementSibling); renumber(); save(); }); on(btnDel,'click', ()=>{ li.remove(); renumber(); save(); }); function update(){ if(timeSpan&&minsInp) timeSpan.textContent = two(minsInp.value)+'m'; if(typeSpan&&typeSel) typeSpan.textContent = typeSel.value; if(titleSpan&&titleInp) titleSpan.textContent = esc(titleInp.value); renumber(); save(); } } function renumber(){ const items = $$('#agenda .block'); const target = Math.max(15, Number(els.duration && els.duration.value || 60)); let total = 0; items.forEach(b=>{ const minsEl=b.querySelector('.b-mins'); const mins = Number(minsEl && minsEl.value || 0); total += Math.max(1, mins); }); if(els.totalMins) els.totalMins.textContent = total; if(els.targetMins) els.targetMins.textContent = target; const status = (total===target? 'ok':'warn'); if(els.totalHelp){ els.totalHelp.classList.remove('ok','warn'); els.totalHelp.classList.add(status); } // Live hints let hint = ''; const diff = total - target; if(diff>0) hint = `You’re +${diff} minutes over. Trim a block or press Rebalance.`; else if(diff addBlock()); // Simple allocation engine const templates = { base(dur, inter, energy){ const blocks=[]; const include = new Set($$('.type').filter(c=>c.checked).map(c=>c.value)); const push=(m,t,ti,ins)=>{ if(m>0) blocks.push([m,t,ti,ins]); }; const short = dur=90; const intro = short? 3:5; const wrap = 5; let remain = dur-intro-wrap; let breakAdded=false; push(intro,'Intro','Welcome & objectives','Set outcomes, agenda, norms.'); if(include.has('icebreaker') && !short) push(6,'Icebreaker','Quick pair warm‑up','Prompt: “What will make this session useful?” 2 min each, then share.'); while(remain>0){ if(include.has('minilecture')){ const m = Math.min( remain>30? 8 : 6, remain); push(m,'Mini‑lecture','Show & tell','Explain with one concrete example.'); remain-=m; } if(remain include.has(k)) || 'discussion'; const map = {pairshare:['Pair‑share','Prompt + 2 min each + harvest'],hands:['Hands‑on','Follow the steps and produce a draft'],case:['Case','Read, decide, justify'],roleplay:['Role‑play','Set roles, 3‑minute rounds, swap, debrief'],discussion:['Discussion','Discuss prompt and capture 3 insights']}; const pair = map[pick]||['Practice','Do the activity']; push(p, pair[0], pair[1], 'Facilitator roams, timebox, and debrief.'); remain-=p; if(!breakAdded && $('#breaks') && $('#breakLen') && $('#breaks').checked && long && remain>30){ const b = Math.min(Number($('#breakLen').value||5), remain); push(b,'Break','Break','Hydrate & move.'); remain-=b; breakAdded=true; } } let sum = blocks.reduce((a,b)=>a+b[0],0); const diff = dur - sum - wrap; if(diff!==0 && blocks.length){ blocks[blocks.length-1][0] = Math.max(1, blocks[blocks.length-1][0] + diff); } push(wrap,'Wrap','Commitments & next steps','Capture actions, owners, dates. Quick feedback.'); return blocks; } }; function generate(){ const dur = Math.max(15, Number(els.duration && els.duration.value || 60)); const inter = els.inter && els.inter.value || 'medium'; const energy = els.energy && els.energy.value || 'balanced'; const blocks = templates.base(dur, inter, energy); if(els.agenda) els.agenda.innerHTML=''; blocks.forEach(([m,t,ti,ins])=> addBlock(m,t,ti,ins)); if(els.targetMins) els.targetMins.textContent = dur; renumber(); save(); } // Robust reset function clearForm(){ const form = $('#wab-form'); let usedNative = false; try{ if(form && typeof form.reset === 'function') { form.reset(); usedNative = true; } }catch(e){} if(!usedNative && form){ form.querySelectorAll('input').forEach(inp=>{ if(inp.type==='checkbox' || inp.type==='radio') inp.checked = inp.defaultChecked; else inp.value = (inp.defaultValue || ''); }); form.querySelectorAll('select').forEach(sel=>{ const def = Array.from(sel.options).find(o=>o.defaultSelected); sel.selectedIndex = def? def.index : 0; }); form.querySelectorAll('textarea').forEach(ta=> ta.value = (ta.defaultValue || '')); } // Explicit defaults if(els.duration) els.duration.value=60; if(els.size) els.size.value=12; if(els.goal) els.goal.value=''; if(els.inter) els.inter.value='medium'; if(els.energy) els.energy.value='balanced'; if($('#breaks')) $('#breaks').checked=true; if($('#breakEvery')) $('#breakEvery').value=60; if($('#breakLen')) $('#breakLen').value=5; $$('.type').forEach(c=> c.checked=true); if(els.agenda) els.agenda.innerHTML=''; if(els.targetMins) els.targetMins.textContent='60'; if(els.totalMins) els.totalMins.textContent='0'; } on(els.generate,'click', generate); on(els.reset,'click', ()=>{ clearForm(); save(); generate(); }); // Rebalance function rebalance(){ const target = Math.max(15, Number(els.duration && els.duration.value || 60)); const items = $$('#agenda .block'); let sum = 0; const mins = items.map(li=> Number(li.querySelector('.b-mins') && li.querySelector('.b-mins').value || 0)); mins.forEach(m=> sum += Math.max(1,m)); if(sum===0) return; const scale = target/sum; const newM = mins.map(m=> Math.max(1, Math.round(m*scale))); const adj = target - newM.reduce((a,b)=>a+b,0); if(newM.length) newM[newM.length-1] = Math.max(1, newM[newM.length-1] + adj); items.forEach((li,i)=>{ const inp=li.querySelector('.b-mins'); const time=li.querySelector('.time'); if(inp) inp.value = newM[i]; if(time) time.textContent = two(newM[i])+'m'; }); renumber(); save(); } on(els.rebalance,'click', rebalance); // Export helpers function toBlocks(){ return $$('#agenda .block').map(li=>({ mins:Number(li.querySelector('.b-mins') && li.querySelector('.b-mins').value || 0), type:li.querySelector('.b-type') && li.querySelector('.b-type').value || '', title:li.querySelector('.b-title') && li.querySelector('.b-title').value || '', ins:li.querySelector('.b-ins') && li.querySelector('.b-ins').value || '' })); } function toText(){ const head = `Title: ${els.title && els.title.value || '-'}\nDuration: ${els.duration && els.duration.value || 60} mins\nOutcome: ${els.goal && els.goal.value || '-'}\n`; const body = toBlocks().map((b,i)=>`${i+1}. [${b.mins}m] ${b.type} — ${b.title}\n ${b.ins}`).join('\n'); return head+'\n'+body; } function toMarkdown(){ return '# Workshop Agenda\n\n'+toText(); } function writeClipboard(txt){ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(txt).then(()=>flash(els.copy)).catch(()=>fallback(txt)); } else fallback(txt); } function fallback(txt){ try{ const ta=document.createElement('textarea'); ta.value=txt; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.left='-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);}catch(e){} flash(els.copy); } function flash(btn){ if(!btn) return; const t=btn.textContent; btn.textContent='Copied!'; setTimeout(()=>btn.textContent=t,1200); } on(els.copy,'click', ()=> writeClipboard(toText())); on(els.download,'click', ()=>{ const md = toMarkdown(); const blob=new Blob([md],{type:'text/markdown'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='agenda.md'; document.body.appendChild(a); a.click(); setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 400); }); // Share state function collectState(){ return { title:els.title&&els.title.value, duration:els.duration&&els.duration.value, size:els.size&&els.size.value, goal:els.goal&&els.goal.value, inter:els.inter&&els.inter.value, energy:els.energy&&els.energy.value, breaks: $('#breaks') && $('#breaks').checked, breakEvery: $('#breakEvery') && $('#breakEvery').value, breakLen: $('#breakLen') && $('#breakLen').value, types: $$('.type').map(c=>({v:c.value, on:c.checked})), blocks: toBlocks() }; } function applyState(st){ if(!st) return; if(els.title) els.title.value=st.title||''; if(els.duration) els.duration.value=st.duration||60; if(els.size) els.size.value=st.size||12; if(els.goal) els.goal.value=st.goal||''; if(els.inter) els.inter.value=st.inter||'medium'; if(els.energy) els.energy.value=st.energy||'balanced'; if($('#breaks')) $('#breaks').checked=!!st.breaks; if($('#breakEvery')) $('#breakEvery').value=st.breakEvery||60; if($('#breakLen')) $('#breakLen').value=st.breakLen||5; $$('.type').forEach(c=>{ const f=(st.types||[]).find(x=>x.v===c.value); if(f) c.checked=!!f.on; }); if(els.agenda) els.agenda.innerHTML=''; (st.blocks||[]).forEach(b=> addBlock(b.mins,b.type,b.title,b.ins)); if(els.targetMins) els.targetMins.textContent = els.duration && els.duration.value || 60; renumber(); } function save(){ store.set('wab_state_v1', collectState()); } ;['input','change'].forEach(ev=>{ ['#title','#duration','#size','#goal','#interactivity','#energy','#breaks','#breakEvery','#breakLen'].forEach(sel=>{ const node=$(sel); on(node, ev, ()=>{ renumber(); save(); }); }); $$('.type').forEach(cb=> on(cb, ev, ()=> save())); }); // Guided tour (friendlier copy) const tour = { steps:[ {sel:'#fld-duration',t:'Set total minutes. The agenda will fit this exactly.'}, {sel:'#fld-goal',t:'Write the one thing people must leave with.'}, {sel:'#fld-inter',t:'Pick how much time should be hands-on.'}, {sel:'#fld-breaks',t:'Add a short break about once per hour.'}, {sel:'#generate',t:'Click generate. Edit blocks, then Rebalance to fit.'} ], i:0 }; function highlight(sel){ const el=$(sel); if(!el) return; el.classList.add('wab-focus'); el.scrollIntoView({behavior:'smooth',block:'center'}); } function unhighlight(sel){ const el=$(sel); if(!el) return; el.classList.remove('wab-focus'); } function startTour(){ tour.i=0; if(els.tour) els.tour.style.display='flex'; if(els.tourText) els.tourText.textContent=tour.steps[0].t; highlight(tour.steps[0].sel); } function nextTour(){ unhighlight(tour.steps[tour.i].sel); tour.i++; if(tour.i>=tour.steps.length){ endTour(); return;} if(els.tourText) els.tourText.textContent=tour.steps[tour.i].t; highlight(tour.steps[tour.i].sel); } function endTour(){ unhighlight(tour.steps[Math.min(tour.i,tour.steps.length-1)].sel); if(els.tour) els.tour.style.display='none'; } on(els.runTour,'click', startTour); on(els.tourNext,'click', nextTour); on(els.tourSkip,'click', endTour); // Load from URL or storage (function init(){ const params = new URLSearchParams(location.search); const fromUrl=params.get('wab'); if(fromUrl){ try{ applyState(JSON.parse(decodeURIComponent(fromUrl))); }catch(e){} } else { applyState(store.get('wab_state_v1', null)); } if(!(els.agenda && els.agenda.children.length)) generate(); })(); // Self-tests (console) (function tests(){ try{ // Generate for different durations if(els.duration){ els.duration.value=30; generate(); } let total30 = $$('#agenda .b-mins').reduce((a,i)=>a+Number(i.value||0),0); console.assert(total30===30,'Agenda sums to 30'); if(els.duration){ els.duration.value=90; generate(); } let total90 = $$('#agenda .b-mins').reduce((a,i)=>a+Number(i.value||0),0); console.assert(total90===90,'Agenda sums to 90'); // Rebalance works if(els.duration){ els.duration.value=60; generate(); } const first = $('#agenda .b-mins'); if(first){ first.value = Number(first.value)+5; } renumber(); rebalance(); const total60 = $$('#agenda .b-mins').reduce((a,i)=>a+Number(i.value||0),0); console.assert(total60===60,'Rebalance to 60'); // Manual reset path (clear only) clearForm(); console.assert(Number(els.duration.value)===60,'Reset sets duration'); console.assert(($$('#agenda .block').length)===0,'Reset clears agenda'); // Reset button regenerates a fresh agenda automatically if(els.reset && typeof els.reset.click === 'function'){ els.reset.click(); const blocksAfterReset = $$('#agenda .block').length; console.assert(blocksAfterReset>0,'Reset regenerates agenda'); const totalAfterReset = $$('#agenda .b-mins').reduce((a,i)=>a+Number(i.value||0),0); const expected = Number(els.duration && els.duration.value || 60); console.assert(totalAfterReset===expected,'Reset agenda sums to duration'); } }catch(e){ console.warn('Self-tests failed', e); } })(); })();