Spaces:
Running
Port the Turing-chat typewriter reveal into the Mission Control consult modal
Browse filesThe 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>
- dee/static/app.js +39 -4
- dee/static/index.html +1 -1
|
@@ -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 |
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 |
-
|
| 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)
|
| 8687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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;
|
|
@@ -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=
|
| 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>
|