← AI

Agent Task Board

A tiny three-column board, To do, Doing and Done, with an AI agent working through it. Each task card glides across into Doing, a spinner turns and a progress bar fills along its bottom edge, then it glides on into Done and pops a green tick. The other cards shuffle up to fill the gap and the column counts tick over as they go.

  1. Read the brief
  2. Search for sources
  3. Draft the outline
  4. Write the summary
  5. Check the facts

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-agent-task-board to an <ol> or <ul>. Each <li> is a task; they’re done in order.

Options

Add any of these attributes to the same element.

data-columnsThe three column names, separated by |. Default To do|Doing|Done.
data-stepHow long each task takes. Default 1800 ms. A task can set its own.
data-colorColour of the progress bar and the working card. Default #7c3aed.
data-loopPut the cards back and work through them again.
data-holdHow long the finished board shows before looping. Default 2400 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Agent Task Board: a little three-column board (To do, Doing, Done). An AI
    // agent picks up each task card in turn: the card glides into Doing, a
    // progress bar fills with a spinner turning, then it glides on into Done and
    // gets a green tick. Column counts tick over as cards move.
    // Usage: <ol data-agent-task-board>
    //          <li>Read the brief</li><li>Draft the outline</li><li>Write the intro</li>
    //        </ol>
    // Optional: data-columns (three column names separated by |, default
    // "To do|Doing|Done"), data-step (ms each task takes, default 1800),
    // data-color (progress and highlight colour, default #7c3aed), data-loop
    // (put the cards back and work through them again), data-hold (ms the
    // finished board shows before looping, default 2400). A task can set its own
    // data-step.
    // Script use: animacious.agentTaskBoard(el) runs it again.
    var DEFAULTS = { columns: 'To do|Doing|Done', step: 1800, color: '#7c3aed', hold: 2400 };

    var css =
      '.atb{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.6em;width:100%;margin:0;padding:0;list-style:none;text-align:left}' +
      '.atb-col{display:flex;flex-direction:column;gap:.45em;min-width:0;padding:.55em;border-radius:.8em;' +
      'background:color-mix(in srgb,var(--text,#1a1a2e) 4%,transparent);border:1px solid color-mix(in srgb,var(--text,#1a1a2e) 7%,transparent)}' +
      '.atb-head{display:flex;align-items:center;gap:.4em;margin:0 0 .1em;font-size:.82em;font-weight:700;letter-spacing:.02em;' +
      'color:color-mix(in srgb,var(--text,#1a1a2e) 65%,transparent);white-space:nowrap}' +
      '.atb-head i{width:.55em;height:.55em;border-radius:50%;flex:none;background:#a1a1aa}' +
      '.atb-col:nth-child(2) .atb-head i{background:var(--atb-c)}' +
      '.atb-col:nth-child(3) .atb-head i{background:#16a34a}' +
      '.atb-n{margin-left:auto;min-width:1.5em;padding:0 .4em;border-radius:99px;text-align:center;font-variant-numeric:tabular-nums;' +
      'background:color-mix(in srgb,var(--text,#1a1a2e) 8%,transparent)}' +
      '.atb-card{position:relative;display:flex;align-items:flex-start;gap:.45em;padding:.5em .6em;border-radius:.55em;overflow:hidden;' +
      'background:var(--bg-card,#fff);color:var(--text,#1a1a2e);border:1px solid color-mix(in srgb,var(--text,#1a1a2e) 10%,transparent);' +
      'box-shadow:0 1px 2px rgba(20,10,40,.06);line-height:1.3;transition:border-color .3s,box-shadow .3s,opacity .3s}' +
      '.atb-card>span{min-width:0;overflow-wrap:break-word;hyphens:auto}' +
      '.atb-mark{position:relative;flex:none;width:1em;height:1em;margin-top:.1em;border-radius:50%;border:.12em solid color-mix(in srgb,var(--text,#1a1a2e) 25%,transparent)}' +
      '.atb-bar{position:absolute;left:0;bottom:0;height:.18em;width:100%;background:var(--atb-c);transform-origin:0 50%;transform:scaleX(0)}' +
      '.atb-doing{border-color:color-mix(in srgb,var(--atb-c) 45%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--atb-c) 22%,transparent)}' +
      '.atb-doing .atb-mark{border-color:color-mix(in srgb,var(--atb-c) 25%,transparent);border-top-color:var(--atb-c);animation:atb-spin .7s linear infinite}' +
      '.atb-done .atb-mark{border-color:#16a34a;background:#16a34a}' +
      '.atb-done .atb-mark::after{content:"";position:absolute;left:28%;top:12%;width:28%;height:52%;border:solid #fff;border-width:0 .13em .13em 0;transform:rotate(45deg)}' +
      '.atb-done>span{opacity:.6}' +
      '@keyframes atb-spin{to{transform:rotate(360deg)}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }

    function setup(el) {
      var d = el.dataset;
      var names = (d.columns || DEFAULTS.columns).split('|');
      var tasks = [].slice.call(el.children).map(function (li) {
        return { text: li.textContent.trim(), step: +li.dataset.step || 0 };
      });
      el.classList.add('atb');
      el.style.setProperty('--atb-c', d.color || DEFAULTS.color);
      el.innerHTML = names.slice(0, 3).map(function (n) {
        return '<li class="atb-col"><p class="atb-head"><i aria-hidden="true"></i>' + esc(n.trim()) +
          '<span class="atb-n">0</span></p></li>';
      }).join('');
      var cols = [].slice.call(el.children);
      var cards = tasks.map(function (t) {
        var c = document.createElement('div');
        c.className = 'atb-card';
        c.innerHTML = '<i class="atb-mark" aria-hidden="true"></i><span>' + esc(t.text) + '</span><i class="atb-bar" aria-hidden="true"></i>';
        c._atbStep = t.step;
        cols[0].appendChild(c);
        return c;
      });
      el._atb = { cols: cols, cards: cards, run: 0, timers: [], anims: [] };
      counts(el._atb);
    }

    function counts(st) {
      st.cols.forEach(function (col) {
        col.querySelector('.atb-n').textContent = col.querySelectorAll('.atb-card').length;
      });
    }

    // Move cards between columns and let every card glide from where it was to
    // where it lands (FLIP: measure, move, then animate the difference away).
    function move(st, changes, ms) {
      var before = st.cards.map(function (c) { return c.getBoundingClientRect(); });
      changes();
      counts(st);
      st.cards.forEach(function (c) {
        if (c._atbA) c._atbA.pause();
        anime.utils.set(c, { x: 0, y: 0 });
      });
      st.cards.forEach(function (c, i) {
        var r = c.getBoundingClientRect();
        var dx = before[i].left - r.left, dy = before[i].top - r.top;
        if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
        c._atbA = anime.animate(c, { x: [dx, 0], y: [dy, 0], duration: ms || 560, ease: 'inOut(3)' });
        st.anims.push(c._atbA);
      });
    }

    function finishAll(st) {
      st.cards.forEach(function (c) {
        c.classList.remove('atb-doing');
        c.classList.add('atb-done');
        anime.utils.set(c.querySelector('.atb-bar'), { scaleX: 0 });
        st.cols[2].appendChild(c);
      });
      counts(st);
    }

    function play(el) {
      var st = el._atb, d = el.dataset;
      var step = +d.step || DEFAULTS.step;
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      var run = ++st.run;
      st.timers.forEach(clearTimeout);
      st.anims.forEach(function (a) { a.pause(); });
      st.timers = [];
      st.anims = [];
      function later(fn, ms) { st.timers.push(setTimeout(function () { if (run === st.run) fn(); }, ms)); }

      st.cards.forEach(function (c) { anime.utils.set(c, { x: 0, y: 0, scale: 1 }); });
      if (st.cards.some(function (c) { return c.parentNode !== st.cols[0]; })) {
        // Send everything back to To do in one sweep.
        move(st, function () {
          st.cards.forEach(function (c) { c.classList.remove('atb-done', 'atb-doing'); st.cols[0].appendChild(c); });
        }, 700);
        later(function () { next(0); }, 1100);
      } else later(function () { next(0); }, 700);

      function next(i) {
        if (i >= st.cards.length) {
          if (d.loop !== undefined) later(function () { play(el); }, hold);
          return;
        }
        var c = st.cards[i];
        var t = c._atbStep || step;
        var bar = c.querySelector('.atb-bar');
        anime.utils.set(bar, { scaleX: 0, opacity: 1 });
        move(st, function () { c.classList.add('atb-doing'); st.cols[1].appendChild(c); });
        st.anims.push(anime.animate(c, { scale: [1, 1.04, 1], duration: 560, ease: 'inOut(2)' }));
        st.anims.push(anime.animate(bar, { scaleX: [0, 1], duration: t - 200, delay: 400, ease: 'inOut(1.4)' }));
        later(function () {
          move(st, function () {
            c.classList.remove('atb-doing');
            c.classList.add('atb-done');
            st.cols[2].appendChild(c);
          });
          st.anims.push(anime.animate(bar, { opacity: 0, duration: 300, ease: 'out(2)' }));
          st.anims.push(anime.animate(c.querySelector('.atb-mark'), { scale: [0.4, 1], duration: 500, delay: 300, ease: 'outBack(2.5)' }));
          later(function () { next(i + 1); }, 520);
        }, t + 400);
      }
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-agent-task-board]').forEach(function (el) {
        if (el._atb) return;
        setup(el);
        if (reduced()) finishAll(el._atb);
        else io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.agentTaskBoard = function (el) { if (el && el._atb) play(el); };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<ol data-agent-task-board>
  <li>Read the brief</li>
  <li>Search for sources</li>
  <li data-step="3000">Draft the outline</li>
  <li>Check the facts</li>
</ol>