File size: 11,602 Bytes
cfcfbaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
const $ = id => document.getElementById(id);
const host = window.frameElement;
if (host) {
  document.body.classList.add('embedded');
  const main = document.querySelector('main');
  new ResizeObserver(() => {
    const margin = parseFloat(getComputedStyle(main).marginBottom);
    host.style.height = `${Math.ceil(main.offsetTop + main.offsetHeight + margin) + 1}px`;
  }).observe(main);
}

let trace, playing = false, frame;
const phases = {rclc:0, text:0};
const cards = {rclc:[], text:[]};
const titles = ['Booking confirmation', 'Location-change notice', 'ID policy'];
const question = 'Where should Maya collect her pass on Friday, and what ID should she bring?';
const reviewedAnswer = 'Maya should collect her pass at **Harbour Hub** on Friday, and she should bring a **passport** as her ID.\n\n**Evidence:**\n- "Reference J7 has moved to Harbour Hub."\n- "Harbour Hub requires a passport for replacement building pass collections."';

function element(tag, text, className) {
  const node = document.createElement(tag);
  if (text !== undefined) node.textContent = text;
  if (className) node.className = className;
  return node;
}

function segments(source, indices) {
  const chars = Array.from(source.text), mask = Array(chars.length).fill(false);
  for (const index of indices) {
    const [start, end] = source.offsets[index];
    for (let i = start; i < end; i++) mask[i] = true;
  }
  const result = [];
  for (let start = 0; start < chars.length;) {
    let end = start + 1;
    while (end < chars.length && mask[end] === mask[start]) end++;
    result.push({text:chars.slice(start, end).join(''), selected:mask[start]});
    start = end;
  }
  return result;
}

function prose(container, text, fraction = 1) {
  const parts = text.split(/(\*\*[^*\n]+\*\*)/g).map(part => {
    const bold = part.startsWith('**') && part.endsWith('**');
    return {bold, chars:Array.from(bold ? part.slice(2, -2) : part)};
  });
  let remaining = Math.floor(parts.reduce((n, part) => n + part.chars.length, 0) * Math.min(1, Math.max(0, fraction)));
  if (container.dataset.characters === String(remaining)) return;
  container.dataset.characters = String(remaining); container.replaceChildren();
  for (const part of parts) {
    const chars = part.chars.slice(0, remaining); remaining -= chars.length;
    if (chars.length) container.append(element(part.bold ? 'strong' : 'span', chars.join('')));
  }
}

function positionMap(source) {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.classList.add('position-map');
  svg.setAttribute('viewBox', `0 0 ${source.offsets.length} 6`);
  svg.setAttribute('preserveAspectRatio', 'none');
  svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', 'Source positions; teal positions are selected');
  source.offsets.forEach((_, i) => {
    const rect = document.createElementNS(svg.namespaceURI, 'rect');
    rect.setAttribute('x', i); rect.setAttribute('y', 0);
    rect.setAttribute('width', '.8'); rect.setAttribute('height', '6');
    svg.append(rect);
  });
  return svg;
}

function makeCards(method) {
  for (let i = 0; i < 4; i++) {
    const answer = i === 3, source = method === 'rclc' && !answer;
    const card = element('div', undefined, `chunk ${answer ? 'answer message-card' : source ? 'source' : 'message-card'}`);
    card.setAttribute('aria-label', answer ? `${method === 'rclc' ? 'CacheBack' : 'Text'} final answer` : source ? `Full source document ${i + 1}: ${titles[i]}` : `Generated message from Agent ${i + 1}`);
    const title = element('p', answer ? 'Final answer' : source ? titles[i] : `Agent ${i + 1}’s message`, 'source-title');
    if (source) title.append(element('span', `Agent ${i + 1}`));
    card.append(title);
    const badge = element('p', '', 'answer-check'); badge.hidden = true;
    if (answer) card.append(badge);
    const body = element('div', undefined, source ? 'document-body' : 'message-body');
    body.tabIndex = 0; card.append(body);
    let map, handoff, label;
    if (source) { map = positionMap(trace.sources[i]); card.append(map); }
    if (!answer) {
      handoff = element('div', undefined, 'handoff');
      label = element('span', 'Waiting'); handoff.append(label);
      const track = element('div', undefined, 'handoff-track'); track.setAttribute('aria-hidden', 'true');
      if (source) {
        const packet = element('div', undefined, 'packet');
        for (let n = 0; n < 4; n++) packet.append(element('i'));
        track.append(packet);
      }
      handoff.append(track); card.append(handoff);
    }
    if (source) card.prepend(map, handoff);
    $(`${method}-row`).append(card);
    cards[method].push({card, body, map, badge, label});
  }
}

function render(method) {
  const phase = phases[method], recorded = trace.cases[0];
  const selected = trace.sources.map(() => []);
  if (method === 'rclc' && (playing || phase)) {
    // Reveal the recorded target selection during each handoff's playback interval.
    const hop = playing && phase < 3 ? phase : Math.min(phase, 3) - 1;
    for (const [source, token] of recorded.rclc.hops[hop].origins) selected[source].push(token);
  }
  cards[method].forEach(({card, body, map, badge, label}, i) => {
    const active = playing && phase === i, passed = phase > i;
    card.classList.toggle('is-selecting', method === 'rclc' && i < 3 && active);
    card.classList.toggle('is-writing', active && (method === 'text' || i === 3));
    card.classList.toggle('has-passed', passed);
    if (method === 'rclc' && i < 3) {
      card.style.setProperty('--selection-duration', `${recorded.rclc.hops[i].seconds / Number($('speed').value)}s`);
      const scroll = body.scrollTop;
      body.replaceChildren(...segments(trace.sources[i], selected[i]).map(piece => element(piece.selected ? 'mark' : 'span', piece.text)));
      body.scrollTop = scroll;
      const indices = new Set(selected[i]);
      [...map.children].forEach((rect, n) => rect.classList.toggle('selected', indices.has(n)));
      label.textContent = passed ? 'Selected state passed' : active ? 'Selecting positions' : 'Full source document';
    } else {
      delete body.dataset.characters;
      if (passed) {
        const message = i === 3 ? recorded[method].answer : recorded.text.hops[i].message;
        prose(body, message.text || '(empty message)');
        body.scrollTop = 0;
      } else {
        body.replaceChildren();
        if (!active) {
          const dots = element('div', undefined, 'waiting-state'); dots.setAttribute('aria-hidden', 'true');
          for (let n = 0; n < 3; n++) dots.append(element('i'));
          body.append(dots, element('p', i === 3 ? 'Waiting for the handoffs.' : 'Waiting to generate a message.', 'placeholder'));
        }
      }
      if (label) label.textContent = passed ? 'Message passed' : active ? 'Generating message' : 'Waiting';
    }
    if (i === 3) {
      const done = phase === 4, reviewed = recorded.question === question;
      card.classList.toggle('complete', done && method === 'rclc');
      const unresolved = done && reviewed && method === 'text' && recorded.text.answer.text.startsWith('Maya should collect her pass at the assigned desk based on her reference J7');
      card.classList.toggle('unresolved', unresolved);
      badge.hidden = true;
      if (done && reviewed && method === 'rclc' && recorded.rclc.answer.text === reviewedAnswer) {
        badge.textContent = '✓ Harbour Hub · Passport'; badge.hidden = false;
      } else if (unresolved) {
        badge.textContent = 'Collection desk unresolved'; badge.hidden = false;
      }
    }
  });
  const activity = $(`${method}-activity`);
  activity.textContent = phase === 4 ? 'Finished' : playing ? phase === 3 ? 'Generating answer' : method === 'rclc' ? 'Selecting & passing' : 'Generating message' : 'Ready';
  activity.classList.toggle('running', playing && phase < 4);
  activity.classList.toggle('complete', phase === 4);
  $(`${method}-time`).textContent = phase === 4 ? `${recorded[method].seconds.toFixed(2)} s` : '0.00 s';
  if (method === 'rclc') document.querySelectorAll('.topology li').forEach((node, i) => node.classList.toggle('reached', phase > i));
  $('status').textContent = `${method === 'rclc' ? 'CacheBack' : 'Text'}: ${phase === 4 ? 'final answer received.' : `${phase} of 3 handoffs received.`}`;
}

function reset() {
  cancelAnimationFrame(frame); playing = false;
  for (const method of ['rclc', 'text']) {
    phases[method] = 0; render(method);
    cards[method].forEach(({body}) => { body.scrollTop = 0; });
  }
  $('play').disabled = $('speed').disabled = false;
  $('play').textContent = 'Run ▶'; $('clear').hidden = true;
}

function play() {
  if (playing || !trace) return;
  reset(); playing = true; $('play').disabled = $('speed').disabled = true;
  $('play').textContent = 'Running';
  $('clear').hidden = false;
  const speed = Number($('speed').value), deadlines = {};
  for (const method of ['rclc', 'text']) {
    let elapsed = 0;
    const run = trace.cases[0][method];
    deadlines[method] = [...run.hops.map(hop => elapsed += hop.seconds * 1000), run.seconds * 1000];
    render(method);
  }
  let start;
  function advance(now) {
    start ??= now;
    const elapsed = (now - start) * speed;
    for (const method of ['rclc', 'text']) {
      const phase = deadlines[method].filter(ms => elapsed >= ms).length;
      if (phase !== phases[method]) { phases[method] = phase; render(method); }
      if (phase < 4) {
        $(`${method}-time`).textContent = `${(elapsed / 1000).toFixed(2)} s`;
        if (method === 'text' || phase === 3) {
          const begin = phase ? deadlines[method][phase - 1] : 0;
          const message = phase === 3 ? trace.cases[0][method].answer : trace.cases[0].text.hops[phase].message;
          const body = cards[method][phase].body;
          prose(body, message.text, (elapsed - begin) / (deadlines[method][phase] - begin));
          body.scrollTop = body.scrollHeight;
        }
      }
    }
    if (phases.rclc < 4 || phases.text < 4) frame = requestAnimationFrame(advance);
    else {
      playing = false; $('play').disabled = $('speed').disabled = false;
      $('play').textContent = 'Replay';
    }
  }
  frame = requestAnimationFrame(advance);
}

$('play').addEventListener('click', play);
$('clear').addEventListener('click', () => { reset(); $('play').focus(); });
$('speed').addEventListener('change', () => {
  $('timing-note').textContent = `${$('speed').value === '1' ? '' : `Playback ${$('speed').value}×. `}Measured times; selection and streaming animated for playback.`;
});

async function load() {
  try {
    const response = await fetch('trace-w4.json');
    if (!response.ok) throw new Error(`Recording unavailable (${response.status}).`);
    trace = await response.json();
    if (trace.schema !== 2 || trace.span_size !== 4 || trace.sources.length !== 3 || !trace.cases.length) throw new Error('Expected a W=4 three-handoff recording.');
    $('question').textContent = trace.cases[0].question;
    const hardware = trace.run.device.startsWith('cuda') ? trace.run.gpu.replace('NVIDIA ', '') : 'CPU';
    $('run-info').textContent = `Recorded ${trace.run.model.split('/').pop()} · ${hardware} · r${trace.ratio} · W=4${trace.complete === false ? ' · Partial recording' : ''}`;
    for (const method of ['rclc', 'text']) { makeCards(method); render(method); }
    $('play').disabled = $('speed').disabled = false;
  } catch (error) {
    $('error').hidden = false;
    $('error').textContent = `${error.message} Serve this folder with python -m http.server.`;
  }
}
load();