Tengo Gzirishvili Claude Sonnet 5 commited on
Commit
1467efe
·
1 Parent(s): ee113d5

Port the Turing-chat typewriter reveal into the Mission Control consult modal

Browse files

The main Turing chat (assistant.js revealText) types replies in
progressively; the Mission Control project-consultation modal (TDConsult in
app.js) painted the full reply instantly instead, an inconsistency the
founder flagged directly. Ports the same pattern into app.js's own context
(the consult modal isn't inside the assistant.html iframe, so it can't reuse
assistant.js's copy directly): a new revealConsultText() writes the reply
into the bubble's .cbody node tick-by-tick at the same pacing (3 chars /
15ms), reusing the existing paragraph-formatting logic via a shared
formatConsultText() helper. renderThread() now wraps assistant bubbles in a
.cbody div so the reveal has a stable node to target without re-rendering
the whole thread mid-type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. dee/static/app.js +39 -4
  2. dee/static/index.html +1 -1
dee/static/app.js CHANGED
@@ -8599,6 +8599,9 @@ function runOracle(opts){
8599
  function el(id) { return document.getElementById(id); }
8600
  function cesc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
8601
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
 
 
 
8602
 
8603
  function openModal() {
8604
  const m = el('consultModal'); if (!m) return;
@@ -8614,12 +8617,32 @@ function runOracle(opts){
8614
  const t = el('consultThread'); if (!t) return;
8615
  t.innerHTML = messages.map((m) => {
8616
  if (m.role === 'user') return '<div class="consult-msg user">' + cesc(m.content) + '</div>';
8617
- const paras = cesc(m.content).split(/\n{2,}/).map((p) => '<p>' + p.replace(/\n/g, '<br>') + '</p>').join('');
8618
- return '<div class="consult-msg turing"><span class="who">Turing</span>' + paras + '</div>';
8619
  }).join('') + (busy ? '<div class="consult-msg thinking">Turing is thinking…</div>' : '');
8620
  t.scrollTop = t.scrollHeight;
8621
  }
8622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8623
  function showBrief(brief) {
8624
  const b = el('consultBrief'); if (!b) return;
8625
  const rows = [['Target', brief.target], ['Property', brief.property], ['Host', brief.host], ['Plan', brief.plan]]
@@ -8683,8 +8706,20 @@ function runOracle(opts){
8683
  const m = text.match(BRIEF_RE);
8684
  let brief = null;
8685
  if (m) { try { brief = JSON.parse(m[1].trim()); } catch (e) {} text = text.replace(BRIEF_RE, '').trim(); }
8686
- if (text) messages.push({ role: 'assistant', content: text });
8687
- renderThread();
 
 
 
 
 
 
 
 
 
 
 
 
8688
  if (brief) showBrief(brief);
8689
  } catch (e) {
8690
  busy = false;
 
8599
  function el(id) { return document.getElementById(id); }
8600
  function cesc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
8601
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
8602
+ function formatConsultText(text) {
8603
+ return cesc(text).split(/\n{2,}/).map((p) => '<p>' + p.replace(/\n/g, '<br>') + '</p>').join('');
8604
+ }
8605
 
8606
  function openModal() {
8607
  const m = el('consultModal'); if (!m) return;
 
8617
  const t = el('consultThread'); if (!t) return;
8618
  t.innerHTML = messages.map((m) => {
8619
  if (m.role === 'user') return '<div class="consult-msg user">' + cesc(m.content) + '</div>';
8620
+ return '<div class="consult-msg turing"><span class="who">Turing</span><div class="cbody">' + formatConsultText(m.content) + '</div></div>';
 
8621
  }).join('') + (busy ? '<div class="consult-msg thinking">Turing is thinking…</div>' : '');
8622
  t.scrollTop = t.scrollHeight;
8623
  }
8624
 
8625
+ // Same Claude/ChatGPT-style progressive reveal as the main Turing chat
8626
+ // (assistant.js revealText) — ported here since the consult modal lives
8627
+ // in the top-level app.js context, not the assistant.html iframe. Writes
8628
+ // directly into the bubble's .cbody node tick-by-tick rather than
8629
+ // re-running renderThread() (which would re-render the whole thread from
8630
+ // `messages` and fight the in-progress reveal).
8631
+ function revealConsultText(bodyEl, fullText) {
8632
+ const CHARS_PER_TICK = 3, TICK_MS = 15;
8633
+ let i = 0;
8634
+ const t = el('consultThread');
8635
+ return new Promise((resolve) => {
8636
+ (function tick() {
8637
+ i = Math.min(fullText.length, i + CHARS_PER_TICK);
8638
+ bodyEl.innerHTML = formatConsultText(fullText.slice(0, i));
8639
+ if (t) t.scrollTop = t.scrollHeight;
8640
+ if (i < fullText.length) setTimeout(tick, TICK_MS);
8641
+ else resolve();
8642
+ })();
8643
+ });
8644
+ }
8645
+
8646
  function showBrief(brief) {
8647
  const b = el('consultBrief'); if (!b) return;
8648
  const rows = [['Target', brief.target], ['Property', brief.property], ['Host', brief.host], ['Plan', brief.plan]]
 
8706
  const m = text.match(BRIEF_RE);
8707
  let brief = null;
8708
  if (m) { try { brief = JSON.parse(m[1].trim()); } catch (e) {} text = text.replace(BRIEF_RE, '').trim(); }
8709
+ if (text) {
8710
+ // Push an empty placeholder first so renderThread() paints the
8711
+ // bubble shell (role label + thinking indicator gone), then type
8712
+ // the real content into that bubble's .cbody node directly —
8713
+ // same two-step pattern assistant.js uses for the main chat.
8714
+ messages.push({ role: 'assistant', content: '' });
8715
+ renderThread();
8716
+ const bubbles = document.querySelectorAll('#consultThread .consult-msg.turing .cbody');
8717
+ const bodyEl = bubbles[bubbles.length - 1];
8718
+ if (bodyEl) await revealConsultText(bodyEl, text);
8719
+ messages[messages.length - 1].content = text;
8720
+ } else {
8721
+ renderThread();
8722
+ }
8723
  if (brief) showBrief(brief);
8724
  } catch (e) {
8725
  busy = false;
dee/static/index.html CHANGED
@@ -2188,7 +2188,7 @@
2188
  <!-- Cloning reference data must load before app.js so the Designer
2189
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2190
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2191
- <script src="/static/app.js?v=20260713-bench4" defer></script>
2192
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2193
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2194
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>
 
2188
  <!-- Cloning reference data must load before app.js so the Designer
2189
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2190
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2191
+ <script src="/static/app.js?v=20260714-consult-typewriter" defer></script>
2192
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2193
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2194
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>