Implement delay timing
This commit is contained in:
@@ -42,7 +42,9 @@ def _is_internal_compose_blob_url(value: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _is_compose_blob_only_text(text_value: str) -> bool:
|
def _is_compose_blob_only_text(text_value: str) -> bool:
|
||||||
lines = [line.strip() for line in str(text_value or "").splitlines() if line.strip()]
|
lines = [
|
||||||
|
line.strip() for line in str(text_value or "").splitlines() if line.strip()
|
||||||
|
]
|
||||||
if not lines:
|
if not lines:
|
||||||
return False
|
return False
|
||||||
return all(_is_internal_compose_blob_url(line) for line in lines)
|
return all(_is_internal_compose_blob_url(line) for line in lines)
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ async def download_and_encode_base64(file_url, filename, content_type, session=N
|
|||||||
return None
|
return None
|
||||||
file_data = await response.read()
|
file_data = await response.read()
|
||||||
base64_encoded = base64.b64encode(file_data).decode("utf-8")
|
base64_encoded = base64.b64encode(file_data).decode("utf-8")
|
||||||
return f"data:{content_type};filename={filename};base64,{base64_encoded}"
|
return (
|
||||||
|
f"data:{content_type};filename={filename};base64,{base64_encoded}"
|
||||||
|
)
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as local_session:
|
async with aiohttp.ClientSession() as local_session:
|
||||||
async with local_session.get(file_url, timeout=10) as response:
|
async with local_session.get(file_url, timeout=10) as response:
|
||||||
@@ -104,7 +106,9 @@ async def send_message_raw(recipient_uuid, text=None, attachments=None):
|
|||||||
file_url = row.get("url")
|
file_url = row.get("url")
|
||||||
if not file_url:
|
if not file_url:
|
||||||
return None
|
return None
|
||||||
return await download_and_encode_base64(file_url, filename, content_type, session)
|
return await download_and_encode_base64(
|
||||||
|
file_url, filename, content_type, session
|
||||||
|
)
|
||||||
|
|
||||||
# Asynchronously resolve and encode all attachments
|
# Asynchronously resolve and encode all attachments
|
||||||
attachments = attachments or []
|
attachments = attachments or []
|
||||||
|
|||||||
@@ -2168,9 +2168,8 @@ class WhatsAppClient(ClientBase):
|
|||||||
if not isinstance(payload, (bytes, bytearray)):
|
if not isinstance(payload, (bytes, bytearray)):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
filename = (
|
filename = self._pluck(msg_obj, "documentMessage", "fileName") or self._pluck(
|
||||||
self._pluck(msg_obj, "documentMessage", "fileName")
|
msg_obj, "document_message", "file_name"
|
||||||
or self._pluck(msg_obj, "document_message", "file_name")
|
|
||||||
)
|
)
|
||||||
content_type = (
|
content_type = (
|
||||||
self._pluck(msg_obj, "documentMessage", "mimetype")
|
self._pluck(msg_obj, "documentMessage", "mimetype")
|
||||||
@@ -2184,7 +2183,9 @@ class WhatsAppClient(ClientBase):
|
|||||||
or self._infer_media_content_type(msg_obj)
|
or self._infer_media_content_type(msg_obj)
|
||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
ext = mimetypes.guess_extension(str(content_type or "").split(";", 1)[0].strip().lower())
|
ext = mimetypes.guess_extension(
|
||||||
|
str(content_type or "").split(";", 1)[0].strip().lower()
|
||||||
|
)
|
||||||
filename = f"wa-{int(time.time())}{ext or '.bin'}"
|
filename = f"wa-{int(time.time())}{ext or '.bin'}"
|
||||||
blob_key = media_bridge.put_blob(
|
blob_key = media_bridge.put_blob(
|
||||||
service="whatsapp",
|
service="whatsapp",
|
||||||
@@ -2749,7 +2750,9 @@ class WhatsAppClient(ClientBase):
|
|||||||
"whatsapp media send ok: method=%s filename=%s ts=%s",
|
"whatsapp media send ok: method=%s filename=%s ts=%s",
|
||||||
send_method,
|
send_method,
|
||||||
filename,
|
filename,
|
||||||
self._normalize_timestamp(self._pluck(response, "Timestamp") or 0),
|
self._normalize_timestamp(
|
||||||
|
self._pluck(response, "Timestamp") or 0
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.warning("whatsapp attachment send failed: %s", exc)
|
self.log.warning("whatsapp attachment send failed: %s", exc)
|
||||||
@@ -2984,7 +2987,9 @@ class WhatsAppClient(ClientBase):
|
|||||||
]
|
]
|
||||||
for args in attempts:
|
for args in attempts:
|
||||||
try:
|
try:
|
||||||
response = await self._call_client_method(method, *args, timeout=9.0)
|
response = await self._call_client_method(
|
||||||
|
method, *args, timeout=9.0
|
||||||
|
)
|
||||||
if response is not None:
|
if response is not None:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"reaction-bridge whatsapp-send ok method=%s args_len=%s",
|
"reaction-bridge whatsapp-send ok method=%s args_len=%s",
|
||||||
|
|||||||
@@ -1188,7 +1188,9 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
recipient_service,
|
recipient_service,
|
||||||
identifier.identifier,
|
identifier.identifier,
|
||||||
emoji=str(reaction_payload.get("emoji") or ""),
|
emoji=str(reaction_payload.get("emoji") or ""),
|
||||||
target_message_id=str((bridge or {}).get("upstream_message_id") or ""),
|
target_message_id=str(
|
||||||
|
(bridge or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
target_timestamp=int((bridge or {}).get("upstream_ts") or 0),
|
target_timestamp=int((bridge or {}).get("upstream_ts") or 0),
|
||||||
target_author=str((bridge or {}).get("upstream_author") or ""),
|
target_author=str((bridge or {}).get("upstream_author") or ""),
|
||||||
remove=bool(reaction_payload.get("remove")),
|
remove=bool(reaction_payload.get("remove")),
|
||||||
@@ -1542,7 +1544,9 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
service=person_identifier.service,
|
service=person_identifier.service,
|
||||||
xmpp_message_id=xmpp_id,
|
xmpp_message_id=xmpp_id,
|
||||||
xmpp_ts=int(time.time() * 1000),
|
xmpp_ts=int(time.time() * 1000),
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
text_preview=str(text or ""),
|
text_preview=str(text or ""),
|
||||||
@@ -1553,9 +1557,13 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
identifier=person_identifier,
|
identifier=person_identifier,
|
||||||
source_service=person_identifier.service,
|
source_service=person_identifier.service,
|
||||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
local_ts=int(
|
||||||
|
(source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)
|
||||||
|
),
|
||||||
xmpp_message_id=xmpp_id,
|
xmpp_message_id=xmpp_id,
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
)
|
)
|
||||||
@@ -1569,7 +1577,9 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
service=person_identifier.service,
|
service=person_identifier.service,
|
||||||
xmpp_message_id=xmpp_id,
|
xmpp_message_id=xmpp_id,
|
||||||
xmpp_ts=int(time.time() * 1000),
|
xmpp_ts=int(time.time() * 1000),
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
text_preview=str(text or ""),
|
text_preview=str(text or ""),
|
||||||
@@ -1580,9 +1590,13 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
identifier=person_identifier,
|
identifier=person_identifier,
|
||||||
source_service=person_identifier.service,
|
source_service=person_identifier.service,
|
||||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
local_ts=int(
|
||||||
|
(source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)
|
||||||
|
),
|
||||||
xmpp_message_id=xmpp_id,
|
xmpp_message_id=xmpp_id,
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
)
|
)
|
||||||
@@ -1611,7 +1625,9 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
service=person_identifier.service,
|
service=person_identifier.service,
|
||||||
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
||||||
xmpp_ts=int(time.time() * 1000),
|
xmpp_ts=int(time.time() * 1000),
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
text_preview=str(row.get("url") or text or ""),
|
text_preview=str(row.get("url") or text or ""),
|
||||||
@@ -1622,13 +1638,21 @@ class XMPPComponent(ComponentXMPP):
|
|||||||
identifier=person_identifier,
|
identifier=person_identifier,
|
||||||
source_service=person_identifier.service,
|
source_service=person_identifier.service,
|
||||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
local_ts=int(
|
||||||
|
(source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)
|
||||||
|
),
|
||||||
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
||||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
upstream_message_id=str(
|
||||||
|
(source_ref or {}).get("upstream_message_id") or ""
|
||||||
|
),
|
||||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||||
)
|
)
|
||||||
return [str(row.get("url") or "").strip() for row in normalized_rows if str(row.get("url") or "").strip()]
|
return [
|
||||||
|
str(row.get("url") or "").strip()
|
||||||
|
for row in normalized_rows
|
||||||
|
if str(row.get("url") or "").strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class XMPPClient(ClientBase):
|
class XMPPClient(ClientBase):
|
||||||
|
|||||||
@@ -247,7 +247,7 @@
|
|||||||
{% with gap=msg.gap_fragments.0 %}
|
{% with gap=msg.gap_fragments.0 %}
|
||||||
<p
|
<p
|
||||||
class="compose-latency-chip"
|
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="icon is-small"><i class="fa-regular fa-clock"></i></span>
|
||||||
<span class="compose-latency-val">{{ gap.lag|default:"-" }}</span>
|
<span class="compose-latency-val">{{ gap.lag|default:"-" }}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -827,6 +827,32 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
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 {
|
#{{ panel_id }} a.compose-glance-item:hover {
|
||||||
border-color: rgba(35, 84, 175, 0.45);
|
border-color: rgba(35, 84, 175, 0.45);
|
||||||
background: rgba(234, 243, 255, 0.96);
|
background: rgba(234, 243, 255, 0.96);
|
||||||
@@ -1290,6 +1316,9 @@
|
|||||||
if (previousState && previousState.timer) {
|
if (previousState && previousState.timer) {
|
||||||
clearInterval(previousState.timer);
|
clearInterval(previousState.timer);
|
||||||
}
|
}
|
||||||
|
if (previousState && previousState.replyTimingTimer) {
|
||||||
|
clearInterval(previousState.replyTimingTimer);
|
||||||
|
}
|
||||||
if (previousState && previousState.eventHandler) {
|
if (previousState && previousState.eventHandler) {
|
||||||
document.body.removeEventListener("composeMessageSent", previousState.eventHandler);
|
document.body.removeEventListener("composeMessageSent", previousState.eventHandler);
|
||||||
}
|
}
|
||||||
@@ -1316,6 +1345,7 @@
|
|||||||
lightboxImages: [],
|
lightboxImages: [],
|
||||||
lightboxIndex: -1,
|
lightboxIndex: -1,
|
||||||
seenMessageIds: new Set(),
|
seenMessageIds: new Set(),
|
||||||
|
replyTimingTimer: null,
|
||||||
};
|
};
|
||||||
window.giaComposePanels[panelId] = panelState;
|
window.giaComposePanels[panelId] = panelState;
|
||||||
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
|
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
|
||||||
@@ -1362,6 +1392,113 @@
|
|||||||
}
|
}
|
||||||
return String(Math.floor(ts / 60000));
|
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 () {
|
const collectLightboxImages = function () {
|
||||||
return Array.from(thread.querySelectorAll(".compose-image"));
|
return Array.from(thread.querySelectorAll(".compose-image"));
|
||||||
};
|
};
|
||||||
@@ -1451,6 +1588,12 @@
|
|||||||
gap: null,
|
gap: null,
|
||||||
metrics: [],
|
metrics: [],
|
||||||
};
|
};
|
||||||
|
let replyTimingState = {
|
||||||
|
sinceLabel: "-",
|
||||||
|
targetLabel: "-",
|
||||||
|
percent: 0,
|
||||||
|
isOverTarget: false,
|
||||||
|
};
|
||||||
const insightUrlForMetric = function (metricSlug) {
|
const insightUrlForMetric = function (metricSlug) {
|
||||||
const slug = String(metricSlug || "").trim();
|
const slug = String(metricSlug || "").trim();
|
||||||
const personId = String(thread.dataset.person || "").trim();
|
const personId = String(thread.dataset.person || "").trim();
|
||||||
@@ -1699,22 +1842,36 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const safe = Array.isArray(items) ? items.slice(0, 3) : [];
|
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 = "";
|
glanceNode.innerHTML = "";
|
||||||
if (!safe.length) {
|
ordered.forEach(function (item) {
|
||||||
glanceNode.classList.add("is-hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
safe.forEach(function (item) {
|
|
||||||
const url = String(item.url || "").trim();
|
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");
|
const chip = document.createElement(url ? "a" : "span");
|
||||||
chip.className = "compose-glance-item";
|
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 || "");
|
chip.title = String(item.tooltip || "");
|
||||||
if (url) {
|
if (url) {
|
||||||
chip.href = url;
|
chip.href = url;
|
||||||
}
|
}
|
||||||
const key = document.createElement("span");
|
const key = document.createElement("span");
|
||||||
key.className = "compose-glance-key";
|
key.className = "compose-glance-key";
|
||||||
key.textContent = String(item.label || "Info");
|
key.textContent = label;
|
||||||
const val = document.createElement("span");
|
const val = document.createElement("span");
|
||||||
val.className = "compose-glance-val";
|
val.className = "compose-glance-val";
|
||||||
val.textContent = String(item.value || "-");
|
val.textContent = String(item.value || "-");
|
||||||
@@ -1722,7 +1879,84 @@
|
|||||||
chip.appendChild(val);
|
chip.appendChild(val);
|
||||||
glanceNode.appendChild(chip);
|
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 () {
|
const updateGlanceFromState = function () {
|
||||||
@@ -1732,10 +1966,10 @@
|
|||||||
glanceState.gap.slug || "inbound_response_score"
|
glanceState.gap.slug || "inbound_response_score"
|
||||||
);
|
);
|
||||||
items.push({
|
items.push({
|
||||||
label: "Response Delay",
|
label: "Delay",
|
||||||
value: String(glanceState.gap.lag || "-") + " · " + String(glanceState.gap.score || "-"),
|
value: String(glanceState.gap.lag || "-") + " · " + String(glanceState.gap.score || "-"),
|
||||||
tooltip: [
|
tooltip: [
|
||||||
String(glanceState.gap.focus || "Response delay"),
|
String(glanceState.gap.focus || "Delay"),
|
||||||
"Delay " + String(glanceState.gap.lag || "-"),
|
"Delay " + String(glanceState.gap.lag || "-"),
|
||||||
"Score " + String(glanceState.gap.score || "-"),
|
"Score " + String(glanceState.gap.score || "-"),
|
||||||
glanceState.gap.calculation ? ("How it is calculated: " + String(glanceState.gap.calculation || "")) : "",
|
glanceState.gap.calculation ? ("How it is calculated: " + String(glanceState.gap.calculation || "")) : "",
|
||||||
@@ -1780,10 +2014,10 @@
|
|||||||
|
|
||||||
const latencyTooltip = function (gap) {
|
const latencyTooltip = function (gap) {
|
||||||
if (!gap || typeof gap !== "object") {
|
if (!gap || typeof gap !== "object") {
|
||||||
return "Response delay between turns.";
|
return "Delay between turns.";
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
String(gap.focus || "Response delay between turns."),
|
String(gap.focus || "Delay between turns."),
|
||||||
"Latency " + String(gap.lag || "-"),
|
"Latency " + String(gap.lag || "-"),
|
||||||
gap.calculation ? ("How it is calculated: " + String(gap.calculation || "")) : "",
|
gap.calculation ? ("How it is calculated: " + String(gap.calculation || "")) : "",
|
||||||
gap.psychology ? ("Psychological interpretation: " + String(gap.psychology || "")) : "",
|
gap.psychology ? ("Psychological interpretation: " + String(gap.psychology || "")) : "",
|
||||||
@@ -2106,6 +2340,7 @@
|
|||||||
applyMinuteGrouping();
|
applyMinuteGrouping();
|
||||||
scrollToBottom(shouldStick);
|
scrollToBottom(shouldStick);
|
||||||
}
|
}
|
||||||
|
updateReplyTimingUi();
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyTyping = function (typingPayload) {
|
const applyTyping = function (typingPayload) {
|
||||||
@@ -2386,6 +2621,7 @@
|
|||||||
panelState.seenMessageIds = new Set();
|
panelState.seenMessageIds = new Set();
|
||||||
glanceState = { gap: null, metrics: [] };
|
glanceState = { gap: null, metrics: [] };
|
||||||
renderGlanceItems([]);
|
renderGlanceItems([]);
|
||||||
|
updateReplyTimingUi();
|
||||||
poll(true);
|
poll(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3339,11 +3575,16 @@
|
|||||||
document.body.addEventListener("composeSendResult", panelState.sendResultHandler);
|
document.body.addEventListener("composeSendResult", panelState.sendResultHandler);
|
||||||
|
|
||||||
hydrateBodyUrlsAsImages(thread);
|
hydrateBodyUrlsAsImages(thread);
|
||||||
|
updateReplyTimingUi();
|
||||||
|
panelState.replyTimingTimer = setInterval(updateReplyTimingUi, 1000);
|
||||||
scrollToBottom(true);
|
scrollToBottom(true);
|
||||||
setupWebSocket();
|
setupWebSocket();
|
||||||
panelState.timer = setInterval(function () {
|
panelState.timer = setInterval(function () {
|
||||||
if (!document.getElementById(panelId)) {
|
if (!document.getElementById(panelId)) {
|
||||||
clearInterval(panelState.timer);
|
clearInterval(panelState.timer);
|
||||||
|
if (panelState.replyTimingTimer) {
|
||||||
|
clearInterval(panelState.replyTimingTimer);
|
||||||
|
}
|
||||||
document.body.removeEventListener("composeMessageSent", panelState.eventHandler);
|
document.body.removeEventListener("composeMessageSent", panelState.eventHandler);
|
||||||
document.body.removeEventListener("composeSendResult", panelState.sendResultHandler);
|
document.body.removeEventListener("composeSendResult", panelState.sendResultHandler);
|
||||||
document.removeEventListener("mousedown", panelState.docClickHandler);
|
document.removeEventListener("mousedown", panelState.docClickHandler);
|
||||||
|
|||||||
@@ -828,7 +828,7 @@ def _glance_items_from_state(gap_fragment=None, metric_fragments=None, person_id
|
|||||||
items = []
|
items = []
|
||||||
if gap_fragment:
|
if gap_fragment:
|
||||||
tooltip_parts = [
|
tooltip_parts = [
|
||||||
f"{gap_fragment.get('focus') or 'Response delay'}",
|
f"{gap_fragment.get('focus') or 'Delay'}",
|
||||||
f"Delay {gap_fragment.get('lag') or '-'}",
|
f"Delay {gap_fragment.get('lag') or '-'}",
|
||||||
f"Score {gap_fragment.get('score') or '-'}",
|
f"Score {gap_fragment.get('score') or '-'}",
|
||||||
]
|
]
|
||||||
@@ -842,7 +842,7 @@ def _glance_items_from_state(gap_fragment=None, metric_fragments=None, person_id
|
|||||||
)
|
)
|
||||||
items.append(
|
items.append(
|
||||||
{
|
{
|
||||||
"label": "Response Delay",
|
"label": "Delay",
|
||||||
"value": f"{gap_fragment.get('lag') or '-'} · {gap_fragment.get('score') or '-'}",
|
"value": f"{gap_fragment.get('lag') or '-'} · {gap_fragment.get('score') or '-'}",
|
||||||
"tooltip": " | ".join(tooltip_parts),
|
"tooltip": " | ".join(tooltip_parts),
|
||||||
"url": _insight_detail_url(
|
"url": _insight_detail_url(
|
||||||
|
|||||||
Reference in New Issue
Block a user