Implement delay timing

This commit is contained in:
2026-02-17 21:49:33 +00:00
parent dc28745fc3
commit eedad846ef
6 changed files with 310 additions and 34 deletions

View File

@@ -247,7 +247,7 @@
{% with gap=msg.gap_fragments.0 %}
<p
class="compose-latency-chip"
title="{{ gap.focus|default:'Response delay between turns.' }} · Latency {{ gap.lag|default:'-' }}{% if gap.calculation %} · How it is calculated: {{ gap.calculation }}{% endif %}{% if gap.psychology %} · Psychological interpretation: {{ gap.psychology }}{% endif %}">
title="{{ gap.focus|default:'Opponent delay between turns.' }} · Latency {{ gap.lag|default:'-' }}{% if gap.calculation %} · How it is calculated: {{ gap.calculation }}{% endif %}{% if gap.psychology %} · Psychological interpretation: {{ gap.psychology }}{% endif %}">
<span class="icon is-small"><i class="fa-regular fa-clock"></i></span>
<span class="compose-latency-val">{{ gap.lag|default:"-" }}</span>
</p>
@@ -827,6 +827,32 @@
color: inherit;
text-decoration: none;
}
#{{ panel_id }} .compose-glance-item.is-equal-size {
width: 10.6rem;
max-width: 10.6rem;
justify-content: space-between;
}
#{{ panel_id }} .compose-glance-item.compose-glance-item-reply {
gap: 0.22rem;
}
#{{ panel_id }} .compose-reply-mini-track {
width: 2.35rem;
height: 0.22rem;
border-radius: 999px;
background: rgba(33, 44, 61, 0.15);
overflow: hidden;
flex: 0 0 auto;
}
#{{ panel_id }} .compose-reply-mini-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: rgba(46, 125, 50, 0.92);
}
#{{ panel_id }} .compose-glance-item.compose-glance-item-reply.is-over-target .compose-reply-mini-fill {
background: rgba(194, 37, 37, 0.92);
}
#{{ panel_id }} a.compose-glance-item:hover {
border-color: rgba(35, 84, 175, 0.45);
background: rgba(234, 243, 255, 0.96);
@@ -1290,6 +1316,9 @@
if (previousState && previousState.timer) {
clearInterval(previousState.timer);
}
if (previousState && previousState.replyTimingTimer) {
clearInterval(previousState.replyTimingTimer);
}
if (previousState && previousState.eventHandler) {
document.body.removeEventListener("composeMessageSent", previousState.eventHandler);
}
@@ -1316,6 +1345,7 @@
lightboxImages: [],
lightboxIndex: -1,
seenMessageIds: new Set(),
replyTimingTimer: null,
};
window.giaComposePanels[panelId] = panelState;
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
@@ -1362,6 +1392,113 @@
}
return String(Math.floor(ts / 60000));
};
const formatElapsedCompact = function (msValue) {
const totalSeconds = Math.max(0, Math.floor((toInt(msValue) || 0) / 1000));
if (totalSeconds < 60) {
return String(totalSeconds) + "s";
}
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes < 60) {
return String(minutes) + "m " + String(seconds) + "s";
}
const hours = Math.floor(minutes / 60);
const remMinutes = minutes % 60;
if (hours < 24) {
return String(hours) + "h " + String(remMinutes) + "m";
}
const days = Math.floor(hours / 24);
const remHours = hours % 24;
return String(days) + "d " + String(remHours) + "h";
};
const collectReplyTimingSnapshot = function () {
const rows = Array.from(thread.querySelectorAll(".compose-row"));
if (!rows.length) {
return null;
}
const timeline = rows
.map(function (row, idx) {
return {
ts: toInt(row && row.dataset ? row.dataset.ts : 0),
outgoing: !!(row && row.classList && row.classList.contains("is-out")),
order: idx,
};
})
.filter(function (item) {
return item.ts > 0;
})
.sort(function (left, right) {
if (left.ts === right.ts) {
return left.order - right.order;
}
return left.ts - right.ts;
});
if (!timeline.length) {
return null;
}
let counterpartBaselineMs = null;
for (let idx = timeline.length - 1; idx > 0; idx -= 1) {
const current = timeline[idx];
const previous = timeline[idx - 1];
if (!current.outgoing && previous.outgoing && current.ts >= previous.ts) {
counterpartBaselineMs = current.ts - previous.ts;
break;
}
}
const last = timeline[timeline.length - 1];
return {
lastTs: last.ts,
isMyTurn: !last.outgoing,
counterpartBaselineMs: counterpartBaselineMs,
};
};
const updateReplyTimingUi = function () {
const snapshot = collectReplyTimingSnapshot();
if (!snapshot || !snapshot.lastTs) {
replyTimingState = {
sinceLabel: "-",
targetLabel: "-",
percent: 0,
isOverTarget: false,
};
renderReplyTimingChip();
return;
}
const elapsedMs = Math.max(0, Date.now() - snapshot.lastTs);
const sinceLabel = formatElapsedCompact(elapsedMs);
const baselineFromGapMs = toInt(
glanceState && glanceState.gap ? glanceState.gap.lag_ms : 0
);
const baselineMs = baselineFromGapMs > 0
? baselineFromGapMs
: toInt(snapshot.counterpartBaselineMs);
if (!baselineMs) {
replyTimingState = {
sinceLabel: sinceLabel,
targetLabel: "pending",
percent: 0,
isOverTarget: false,
};
renderReplyTimingChip();
return;
}
const ratio = elapsedMs / baselineMs;
const percent = Math.max(0, Math.round(ratio * 100));
replyTimingState = {
sinceLabel: sinceLabel,
targetLabel: formatElapsedCompact(baselineMs),
percent: percent,
isOverTarget: ratio > 1,
};
renderReplyTimingChip();
ensurePriorityGlanceOrder();
};
const collectLightboxImages = function () {
return Array.from(thread.querySelectorAll(".compose-image"));
};
@@ -1451,6 +1588,12 @@
gap: null,
metrics: [],
};
let replyTimingState = {
sinceLabel: "-",
targetLabel: "-",
percent: 0,
isOverTarget: false,
};
const insightUrlForMetric = function (metricSlug) {
const slug = String(metricSlug || "").trim();
const personId = String(thread.dataset.person || "").trim();
@@ -1699,22 +1842,36 @@
return;
}
const safe = Array.isArray(items) ? items.slice(0, 3) : [];
const ordered = safe
.filter(function (item) {
return /^delay$/i.test(String(item && item.label ? item.label : ""));
})
.concat(
safe.filter(function (item) {
return !/^delay$/i.test(String(item && item.label ? item.label : ""));
})
);
glanceNode.innerHTML = "";
if (!safe.length) {
glanceNode.classList.add("is-hidden");
return;
}
safe.forEach(function (item) {
ordered.forEach(function (item) {
const url = String(item.url || "").trim();
const label = String(item.label || "Info");
const isStabilityConfidence = /stability\s+confidence/i.test(label);
const isDelayLabel = /^delay$/i.test(label) || /^opponent\s+delay$/i.test(label);
const chip = document.createElement(url ? "a" : "span");
chip.className = "compose-glance-item";
if (isStabilityConfidence) {
chip.classList.add("is-stability-confidence", "is-equal-size");
}
if (isDelayLabel) {
chip.classList.add("is-delay");
}
chip.title = String(item.tooltip || "");
if (url) {
chip.href = url;
}
const key = document.createElement("span");
key.className = "compose-glance-key";
key.textContent = String(item.label || "Info");
key.textContent = label;
const val = document.createElement("span");
val.className = "compose-glance-val";
val.textContent = String(item.value || "-");
@@ -1722,7 +1879,84 @@
chip.appendChild(val);
glanceNode.appendChild(chip);
});
glanceNode.classList.remove("is-hidden");
renderReplyTimingChip();
ensurePriorityGlanceOrder();
if (glanceNode.children.length) {
glanceNode.classList.remove("is-hidden");
} else {
glanceNode.classList.add("is-hidden");
}
};
const ensurePriorityGlanceOrder = function () {
if (!glanceNode) {
return;
}
const children = Array.from(glanceNode.querySelectorAll(".compose-glance-item"));
const delayChip = children.find(function (chip) {
if (chip.classList.contains("is-delay")) {
return true;
}
const key = chip.querySelector(".compose-glance-key");
return /^delay$/i.test(String(key ? key.textContent : "").trim());
}) || null;
const replyChip = glanceNode.querySelector(".compose-glance-item-reply");
if (delayChip) {
glanceNode.insertBefore(delayChip, glanceNode.firstChild);
}
if (replyChip && delayChip) {
glanceNode.insertBefore(replyChip, delayChip.nextSibling);
} else if (replyChip) {
glanceNode.insertBefore(replyChip, glanceNode.firstChild);
}
};
const renderReplyTimingChip = function () {
if (!glanceNode) {
return;
}
let chip = glanceNode.querySelector(".compose-glance-item-reply");
if (!chip) {
chip = document.createElement("span");
chip.className = "compose-glance-item compose-glance-item-reply is-equal-size";
const key = document.createElement("span");
key.className = "compose-glance-key";
key.textContent = "Elapsed";
chip.appendChild(key);
const value = document.createElement("span");
value.className = "compose-glance-val";
value.dataset.role = "reply-value";
value.textContent = "-";
chip.appendChild(value);
const track = document.createElement("span");
track.className = "compose-reply-mini-track";
const fill = document.createElement("span");
fill.className = "compose-reply-mini-fill";
fill.dataset.role = "reply-fill";
track.appendChild(fill);
chip.appendChild(track);
glanceNode.appendChild(chip);
}
const valueNode = chip.querySelector('[data-role="reply-value"]');
const fillNode = chip.querySelector('[data-role="reply-fill"]');
if (valueNode) {
valueNode.textContent = String(replyTimingState.sinceLabel || "-") + " · " + String(replyTimingState.percent || 0) + "%";
}
if (fillNode) {
fillNode.style.width = String(Math.max(0, Math.min(100, toInt(replyTimingState.percent)))) + "%";
}
chip.title = "Last message: " + String(replyTimingState.sinceLabel || "-")
+ " | Target: " + String(replyTimingState.targetLabel || "-")
+ " | Progress: " + String(replyTimingState.percent || 0) + "%";
chip.classList.toggle("is-over-target", !!replyTimingState.isOverTarget);
};
const updateGlanceFromState = function () {
@@ -1732,10 +1966,10 @@
glanceState.gap.slug || "inbound_response_score"
);
items.push({
label: "Response Delay",
label: "Delay",
value: String(glanceState.gap.lag || "-") + " · " + String(glanceState.gap.score || "-"),
tooltip: [
String(glanceState.gap.focus || "Response delay"),
String(glanceState.gap.focus || "Delay"),
"Delay " + String(glanceState.gap.lag || "-"),
"Score " + String(glanceState.gap.score || "-"),
glanceState.gap.calculation ? ("How it is calculated: " + String(glanceState.gap.calculation || "")) : "",
@@ -1780,10 +2014,10 @@
const latencyTooltip = function (gap) {
if (!gap || typeof gap !== "object") {
return "Response delay between turns.";
return "Delay between turns.";
}
return [
String(gap.focus || "Response delay between turns."),
String(gap.focus || "Delay between turns."),
"Latency " + String(gap.lag || "-"),
gap.calculation ? ("How it is calculated: " + String(gap.calculation || "")) : "",
gap.psychology ? ("Psychological interpretation: " + String(gap.psychology || "")) : "",
@@ -2106,6 +2340,7 @@
applyMinuteGrouping();
scrollToBottom(shouldStick);
}
updateReplyTimingUi();
};
const applyTyping = function (typingPayload) {
@@ -2386,6 +2621,7 @@
panelState.seenMessageIds = new Set();
glanceState = { gap: null, metrics: [] };
renderGlanceItems([]);
updateReplyTimingUi();
poll(true);
};
@@ -3339,11 +3575,16 @@
document.body.addEventListener("composeSendResult", panelState.sendResultHandler);
hydrateBodyUrlsAsImages(thread);
updateReplyTimingUi();
panelState.replyTimingTimer = setInterval(updateReplyTimingUi, 1000);
scrollToBottom(true);
setupWebSocket();
panelState.timer = setInterval(function () {
if (!document.getElementById(panelId)) {
clearInterval(panelState.timer);
if (panelState.replyTimingTimer) {
clearInterval(panelState.replyTimingTimer);
}
document.body.removeEventListener("composeMessageSent", panelState.eventHandler);
document.body.removeEventListener("composeSendResult", panelState.sendResultHandler);
document.removeEventListener("mousedown", panelState.docClickHandler);