Renovate mitigation panels and messages
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
{{ service|title }} · {{ identifier }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons are-small" style="margin: 0;">
|
||||
<div class="buttons are-small compose-top-actions" style="margin: 0;">
|
||||
<button type="button" class="button is-light is-rounded js-ai-trigger" data-kind="drafts">
|
||||
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
|
||||
<span>Drafts</span>
|
||||
@@ -34,6 +34,12 @@
|
||||
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
|
||||
<span>AI Workspace</span>
|
||||
</a>
|
||||
{% if render_mode == "page" %}
|
||||
<a class="button is-light is-rounded" href="{{ compose_workspace_url }}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
|
||||
<span>Open In Workspace</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,6 +110,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{{ panel_id }}-glance"
|
||||
class="compose-glance{% if not glance_items %} is-hidden{% endif %}">
|
||||
{% for item in glance_items %}
|
||||
{% if item.url %}
|
||||
<a class="compose-glance-item" href="{{ item.url }}" title="{{ item.tooltip }}">
|
||||
<span class="compose-glance-key">{{ item.label }}</span>
|
||||
<span class="compose-glance-val">{{ item.value }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="compose-glance-item" title="{{ item.tooltip }}">
|
||||
<span class="compose-glance-key">{{ item.label }}</span>
|
||||
<span class="compose-glance-val">{{ item.value }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{{ panel_id }}-thread"
|
||||
class="compose-thread"
|
||||
@@ -122,23 +146,14 @@
|
||||
{% for msg in serialized_messages %}
|
||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}">
|
||||
{% if msg.gap_fragments %}
|
||||
<div class="compose-gap-artifacts">
|
||||
{% for frag in msg.gap_fragments %}
|
||||
<article class="compose-artifact compose-artifact-gap">
|
||||
<p class="compose-artifact-head">
|
||||
<span class="icon is-small"><i class="fa-solid fa-hourglass-half"></i></span>
|
||||
<span>{{ frag.focus }} · {{ frag.lag }}</span>
|
||||
<span class="compose-artifact-score">Score {{ frag.score }}</span>
|
||||
</p>
|
||||
{% if frag.calculation %}
|
||||
<p class="compose-artifact-detail">How: {{ frag.calculation }}</p>
|
||||
{% endif %}
|
||||
{% if frag.psychology %}
|
||||
<p class="compose-artifact-detail">Meaning: {{ frag.psychology }}</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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 %}">
|
||||
<span class="icon is-small"><i class="fa-regular fa-clock"></i></span>
|
||||
<span class="compose-latency-val">{{ gap.lag|default:"-" }}</span>
|
||||
</p>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
|
||||
{% if msg.image_urls %}
|
||||
@@ -171,21 +186,6 @@
|
||||
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
||||
</p>
|
||||
</article>
|
||||
{% if msg.metric_fragments %}
|
||||
<div class="compose-metric-artifacts">
|
||||
{% for frag in msg.metric_fragments %}
|
||||
<article
|
||||
class="compose-artifact compose-artifact-metric"
|
||||
title="How it is calculated: {{ frag.calculation }}{% if frag.psychology %} | Psychological interpretation: {{ frag.psychology }}{% endif %}">
|
||||
<p class="compose-artifact-head">
|
||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||
<span>{{ frag.title }}</span>
|
||||
<span class="compose-artifact-score">{{ frag.value }}</span>
|
||||
</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="compose-empty">No stored messages for this contact yet.</p>
|
||||
@@ -206,6 +206,7 @@
|
||||
<input type="hidden" name="identifier" value="{{ identifier }}">
|
||||
<input type="hidden" name="render_mode" value="{{ render_mode }}">
|
||||
<input type="hidden" name="limit" value="{{ limit }}">
|
||||
<input type="hidden" name="panel_id" value="{{ panel_id }}">
|
||||
<input type="hidden" name="failsafe_arm" value="0">
|
||||
<input type="hidden" name="failsafe_confirm" value="0">
|
||||
{% if person %}
|
||||
@@ -234,6 +235,7 @@
|
||||
<style>
|
||||
#{{ panel_id }}.compose-shell {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
border: 1px solid rgba(0, 0, 0, 0.16);
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
@@ -263,6 +265,57 @@
|
||||
#{{ panel_id }} .compose-row.is-out {
|
||||
align-items: flex-end;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-middle,
|
||||
#{{ panel_id }} .compose-row.is-group-first {
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-middle .compose-msg-meta,
|
||||
#{{ panel_id }} .compose-row.is-group-first .compose-msg-meta {
|
||||
display: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-first .compose-bubble.is-in {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-middle .compose-bubble.is-in {
|
||||
border-radius: 5px 8px 8px 5px;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-last .compose-bubble.is-in {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-first .compose-bubble.is-out {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-middle .compose-bubble.is-out {
|
||||
border-radius: 8px 5px 5px 8px;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-last .compose-bubble.is-out {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
#{{ panel_id }} .compose-latency-chip {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
padding: 0.02rem 0.28rem;
|
||||
border-radius: 999px;
|
||||
color: #6b7787;
|
||||
font-size: 0.61rem;
|
||||
line-height: 1.1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
background: rgba(247, 249, 252, 0.88);
|
||||
border: 1px solid rgba(103, 121, 145, 0.16);
|
||||
}
|
||||
#{{ panel_id }} .compose-latency-val {
|
||||
font-weight: 600;
|
||||
color: #58667a;
|
||||
}
|
||||
#{{ panel_id }} .compose-latency-chip::before,
|
||||
#{{ panel_id }} .compose-latency-chip::after {
|
||||
content: "";
|
||||
width: 0.95rem;
|
||||
height: 1px;
|
||||
background: rgba(101, 119, 141, 0.28);
|
||||
}
|
||||
#{{ panel_id }} .compose-bubble {
|
||||
max-width: min(85%, 46rem);
|
||||
border-radius: 8px;
|
||||
@@ -319,6 +372,35 @@
|
||||
}
|
||||
#{{ panel_id }} .compose-artifact.compose-artifact-gap {
|
||||
margin-bottom: 0.2rem;
|
||||
padding: 0.18rem 0.3rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-gap-line {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
font-size: 0.62rem;
|
||||
line-height: 1.1;
|
||||
color: #4d5b70;
|
||||
}
|
||||
#{{ panel_id }} .compose-gap-lag,
|
||||
#{{ panel_id }} .compose-gap-score {
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .compose-gap-track {
|
||||
flex: 1 1 auto;
|
||||
min-width: 2.8rem;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: rgba(83, 103, 130, 0.23);
|
||||
overflow: hidden;
|
||||
}
|
||||
#{{ panel_id }} .compose-gap-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, rgba(80, 128, 196, 0.95), rgba(31, 95, 176, 0.95));
|
||||
}
|
||||
#{{ panel_id }} .compose-artifact-head {
|
||||
margin: 0;
|
||||
@@ -399,6 +481,46 @@
|
||||
margin-top: 0.55rem;
|
||||
min-height: 1.1rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-glance {
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.45rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.28rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-glance.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-glance-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
max-width: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.14);
|
||||
border-radius: 999px;
|
||||
padding: 0.12rem 0.45rem;
|
||||
background: rgba(250, 252, 255, 0.95);
|
||||
font-size: 0.64rem;
|
||||
line-height: 1.2;
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
#{{ panel_id }} a.compose-glance-item:hover {
|
||||
border-color: rgba(35, 84, 175, 0.45);
|
||||
background: rgba(234, 243, 255, 0.96);
|
||||
}
|
||||
#{{ panel_id }} .compose-glance-key {
|
||||
color: #5b6a7c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#{{ panel_id }} .compose-glance-val {
|
||||
color: #26384f;
|
||||
font-weight: 700;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
#{{ panel_id }} .compose-status-line {
|
||||
margin: 0;
|
||||
font-size: 0.76rem;
|
||||
@@ -413,17 +535,19 @@
|
||||
}
|
||||
#{{ panel_id }} .compose-ai-popover {
|
||||
position: absolute;
|
||||
top: 4.2rem;
|
||||
right: 0.7rem;
|
||||
width: min(34rem, calc(100% - 1.4rem));
|
||||
z-index: 25;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(40rem, calc(100% - 1rem));
|
||||
margin-top: 0;
|
||||
z-index: 35;
|
||||
overflow: auto;
|
||||
}
|
||||
#{{ panel_id }} .compose-ai-popover-backdrop {
|
||||
position: absolute;
|
||||
inset: 0.45rem;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
background: radial-gradient(circle at 100% 0%, rgba(238, 245, 255, 0.42), rgba(255, 255, 255, 0.15) 42%, rgba(255, 255, 255, 0));
|
||||
z-index: 24;
|
||||
z-index: 34;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transition: opacity 140ms ease-out;
|
||||
@@ -477,18 +601,22 @@
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
line-height: 1.35;
|
||||
padding: 0.58rem 0.62rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-draft-tone {
|
||||
margin: 0 0 0.2rem 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 0.28rem 0;
|
||||
color: #6e7782;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
#{{ panel_id }} .compose-draft-text {
|
||||
margin: 0;
|
||||
@@ -502,6 +630,21 @@
|
||||
#{{ panel_id }} .buttons {
|
||||
overflow: visible;
|
||||
}
|
||||
#{{ panel_id }} .compose-top-actions {
|
||||
align-items: stretch;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-top-actions .button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.55rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-top-actions .button > span:last-child {
|
||||
white-space: normal;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
}
|
||||
#{{ panel_id }} .js-ai-trigger {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
@@ -575,18 +718,22 @@
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.32rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-meta {
|
||||
display: inline-flex;
|
||||
@@ -594,6 +741,20 @@
|
||||
gap: 0.44rem;
|
||||
font-size: 0.68rem;
|
||||
color: #657283;
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.14rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-meta > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-body {
|
||||
display: flex;
|
||||
@@ -672,9 +833,20 @@
|
||||
display: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-ai-popover {
|
||||
left: 0.7rem;
|
||||
right: 0.7rem;
|
||||
width: auto;
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-label {
|
||||
width: 100%;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-meta {
|
||||
font-size: 0.64rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
#{{ panel_id }} .compose-top-actions .button {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -695,6 +867,7 @@
|
||||
|
||||
const statusBox = document.getElementById(panelId + "-status");
|
||||
const typingNode = document.getElementById(panelId + "-typing");
|
||||
const glanceNode = document.getElementById(panelId + "-glance");
|
||||
const popover = document.getElementById(panelId + "-popover");
|
||||
const popoverBackdrop = document.getElementById(panelId + "-popover-backdrop");
|
||||
const csrfToken = "{{ csrf_token }}";
|
||||
@@ -713,6 +886,9 @@
|
||||
if (previousState && previousState.docClickHandler) {
|
||||
document.removeEventListener("mousedown", previousState.docClickHandler);
|
||||
}
|
||||
if (previousState && previousState.resizeHandler) {
|
||||
window.removeEventListener("resize", previousState.resizeHandler);
|
||||
}
|
||||
const panelState = {
|
||||
timer: null,
|
||||
polling: false,
|
||||
@@ -724,12 +900,68 @@
|
||||
window.giaComposePanels[panelId] = panelState;
|
||||
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
|
||||
|
||||
const positionPopover = function (kind) {
|
||||
if (!popover || popover.classList.contains("is-hidden")) {
|
||||
return;
|
||||
}
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const statusRect = statusBox ? statusBox.getBoundingClientRect() : null;
|
||||
const trigger = triggerButtons.find(function (button) {
|
||||
return button.dataset.kind === kind;
|
||||
}) || triggerButtons[0] || null;
|
||||
const triggerRect = trigger ? trigger.getBoundingClientRect() : null;
|
||||
const anchorBottom = Math.max(
|
||||
statusRect ? statusRect.bottom : panelRect.top,
|
||||
triggerRect ? triggerRect.bottom : panelRect.top
|
||||
);
|
||||
const top = Math.max(8, Math.round(anchorBottom - panelRect.top + 8));
|
||||
const maxWidth = Math.max(260, Math.round(panelRect.width - 16));
|
||||
const width = Math.min(maxWidth, 640);
|
||||
let left = 8;
|
||||
if (triggerRect) {
|
||||
const triggerCenter = (triggerRect.left + (triggerRect.width / 2)) - panelRect.left;
|
||||
left = Math.round(triggerCenter - (width / 2));
|
||||
}
|
||||
left = Math.max(8, Math.min(left, Math.round(panelRect.width - width - 8)));
|
||||
const maxHeight = Math.max(140, Math.round(panelRect.height - top - 8));
|
||||
popover.style.top = String(top) + "px";
|
||||
popover.style.left = String(left) + "px";
|
||||
popover.style.width = String(width) + "px";
|
||||
popover.style.maxHeight = String(maxHeight) + "px";
|
||||
};
|
||||
|
||||
const toInt = function (value) {
|
||||
const parsed = parseInt(value || "0", 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
|
||||
const minuteBucketFromTs = function (tsValue) {
|
||||
const ts = toInt(tsValue);
|
||||
if (!ts) {
|
||||
return "";
|
||||
}
|
||||
return String(Math.floor(ts / 60000));
|
||||
};
|
||||
|
||||
let lastTs = toInt(thread.dataset.lastTs);
|
||||
let glanceState = {
|
||||
gap: null,
|
||||
metrics: [],
|
||||
};
|
||||
const personId = String(thread.dataset.person || "").trim();
|
||||
const insightUrlForMetric = function (metricSlug) {
|
||||
const slug = String(metricSlug || "").trim();
|
||||
if (!personId || !slug) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
"/ai/workspace/page/person/"
|
||||
+ encodeURIComponent(personId)
|
||||
+ "/insights/"
|
||||
+ encodeURIComponent(slug)
|
||||
+ "/"
|
||||
);
|
||||
};
|
||||
|
||||
const autosize = function () {
|
||||
textarea.style.height = "auto";
|
||||
@@ -838,93 +1070,134 @@
|
||||
wireImageFallbacks(scope);
|
||||
};
|
||||
|
||||
const renderGlanceItems = function (items) {
|
||||
if (!glanceNode) {
|
||||
return;
|
||||
}
|
||||
const safe = Array.isArray(items) ? items.slice(0, 3) : [];
|
||||
glanceNode.innerHTML = "";
|
||||
if (!safe.length) {
|
||||
glanceNode.classList.add("is-hidden");
|
||||
return;
|
||||
}
|
||||
safe.forEach(function (item) {
|
||||
const url = String(item.url || "").trim();
|
||||
const chip = document.createElement(url ? "a" : "span");
|
||||
chip.className = "compose-glance-item";
|
||||
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");
|
||||
const val = document.createElement("span");
|
||||
val.className = "compose-glance-val";
|
||||
val.textContent = String(item.value || "-");
|
||||
chip.appendChild(key);
|
||||
chip.appendChild(val);
|
||||
glanceNode.appendChild(chip);
|
||||
});
|
||||
glanceNode.classList.remove("is-hidden");
|
||||
};
|
||||
|
||||
const updateGlanceFromState = function () {
|
||||
const items = [];
|
||||
if (glanceState.gap) {
|
||||
const gapMetricSlug = String(
|
||||
glanceState.gap.slug || "inbound_response_score"
|
||||
);
|
||||
items.push({
|
||||
label: "Response Delay",
|
||||
value: String(glanceState.gap.lag || "-") + " · " + String(glanceState.gap.score || "-"),
|
||||
tooltip: [
|
||||
String(glanceState.gap.focus || "Response delay"),
|
||||
"Delay " + String(glanceState.gap.lag || "-"),
|
||||
"Score " + String(glanceState.gap.score || "-"),
|
||||
glanceState.gap.calculation ? ("How it is calculated: " + String(glanceState.gap.calculation || "")) : "",
|
||||
glanceState.gap.psychology ? ("Psychological interpretation: " + String(glanceState.gap.psychology || "")) : "",
|
||||
].filter(Boolean).join(" | "),
|
||||
url: insightUrlForMetric(gapMetricSlug),
|
||||
});
|
||||
}
|
||||
(glanceState.metrics || []).slice(0, 2).forEach(function (metric) {
|
||||
const metricSlug = String(metric.slug || "").trim();
|
||||
items.push({
|
||||
label: String(metric.title || "Metric"),
|
||||
value: String(metric.value || "-"),
|
||||
tooltip: [
|
||||
metric.calculation ? ("How it is calculated: " + String(metric.calculation || "")) : "",
|
||||
metric.psychology ? ("Psychological interpretation: " + String(metric.psychology || "")) : "",
|
||||
].filter(Boolean).join(" | "),
|
||||
url: insightUrlForMetric(metricSlug),
|
||||
});
|
||||
});
|
||||
renderGlanceItems(items);
|
||||
};
|
||||
|
||||
const updateGlanceFromMessage = function (msg) {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
if (Array.isArray(msg.gap_fragments) && msg.gap_fragments.length) {
|
||||
glanceState.gap = msg.gap_fragments[0];
|
||||
changed = true;
|
||||
}
|
||||
if (Array.isArray(msg.metric_fragments) && msg.metric_fragments.length) {
|
||||
glanceState.metrics = msg.metric_fragments.slice(0, 2);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
updateGlanceFromState();
|
||||
};
|
||||
|
||||
const latencyTooltip = function (gap) {
|
||||
if (!gap || typeof gap !== "object") {
|
||||
return "Response delay between turns.";
|
||||
}
|
||||
return [
|
||||
String(gap.focus || "Response delay between turns."),
|
||||
"Latency " + String(gap.lag || "-"),
|
||||
gap.calculation ? ("How it is calculated: " + String(gap.calculation || "")) : "",
|
||||
gap.psychology ? ("Psychological interpretation: " + String(gap.psychology || "")) : "",
|
||||
].filter(Boolean).join(" | ");
|
||||
};
|
||||
|
||||
const appendLatencyChip = function (row, msg) {
|
||||
if (!row || !msg || !Array.isArray(msg.gap_fragments) || !msg.gap_fragments.length) {
|
||||
return;
|
||||
}
|
||||
const gap = msg.gap_fragments[0] || {};
|
||||
const lag = String(gap.lag || "").trim();
|
||||
if (!lag) {
|
||||
return;
|
||||
}
|
||||
const chip = document.createElement("p");
|
||||
chip.className = "compose-latency-chip";
|
||||
chip.title = latencyTooltip(gap);
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "icon is-small";
|
||||
const iconGlyph = document.createElement("i");
|
||||
iconGlyph.className = "fa-regular fa-clock";
|
||||
icon.appendChild(iconGlyph);
|
||||
const value = document.createElement("span");
|
||||
value.className = "compose-latency-val";
|
||||
value.textContent = lag;
|
||||
chip.appendChild(icon);
|
||||
chip.appendChild(value);
|
||||
row.appendChild(chip);
|
||||
};
|
||||
|
||||
const appendBubble = function (msg) {
|
||||
const row = document.createElement("div");
|
||||
const outgoing = !!msg.outgoing;
|
||||
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
|
||||
row.dataset.ts = String(msg.ts || 0);
|
||||
|
||||
const appendGapArtifacts = function (fragments) {
|
||||
if (!Array.isArray(fragments) || !fragments.length) {
|
||||
return;
|
||||
}
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "compose-gap-artifacts";
|
||||
fragments.forEach(function (fragment) {
|
||||
const artifact = document.createElement("article");
|
||||
artifact.className = "compose-artifact compose-artifact-gap";
|
||||
const head = document.createElement("p");
|
||||
head.className = "compose-artifact-head";
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "icon is-small";
|
||||
icon.innerHTML = '<i class="fa-solid fa-hourglass-half"></i>';
|
||||
const focus = document.createElement("span");
|
||||
const focusText = String(fragment.focus || "Response gap");
|
||||
const lagText = String(fragment.lag || "");
|
||||
focus.textContent = lagText ? (focusText + " · " + lagText) : focusText;
|
||||
const score = document.createElement("span");
|
||||
score.className = "compose-artifact-score";
|
||||
score.textContent = "Score " + String(fragment.score || "-");
|
||||
head.appendChild(icon);
|
||||
head.appendChild(focus);
|
||||
head.appendChild(score);
|
||||
artifact.appendChild(head);
|
||||
if (fragment.calculation) {
|
||||
const calc = document.createElement("p");
|
||||
calc.className = "compose-artifact-detail";
|
||||
calc.textContent = "How: " + String(fragment.calculation || "");
|
||||
artifact.appendChild(calc);
|
||||
}
|
||||
if (fragment.psychology) {
|
||||
const psych = document.createElement("p");
|
||||
psych.className = "compose-artifact-detail";
|
||||
psych.textContent = "Meaning: " + String(fragment.psychology || "");
|
||||
artifact.appendChild(psych);
|
||||
}
|
||||
wrap.appendChild(artifact);
|
||||
});
|
||||
row.appendChild(wrap);
|
||||
};
|
||||
|
||||
const appendMetricArtifacts = function (fragments) {
|
||||
if (!Array.isArray(fragments) || !fragments.length) {
|
||||
return;
|
||||
}
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "compose-metric-artifacts";
|
||||
fragments.forEach(function (fragment) {
|
||||
const artifact = document.createElement("article");
|
||||
artifact.className = "compose-artifact compose-artifact-metric";
|
||||
const calc = String(fragment.calculation || "");
|
||||
const psych = String(fragment.psychology || "");
|
||||
const tips = [];
|
||||
if (calc) {
|
||||
tips.push("How it is calculated: " + calc);
|
||||
}
|
||||
if (psych) {
|
||||
tips.push("Psychological interpretation: " + psych);
|
||||
}
|
||||
artifact.title = tips.join(" | ");
|
||||
const head = document.createElement("p");
|
||||
head.className = "compose-artifact-head";
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "icon is-small";
|
||||
icon.innerHTML = '<i class="fa-solid fa-chart-line"></i>';
|
||||
const title = document.createElement("span");
|
||||
title.textContent = String(fragment.title || "Metric");
|
||||
const value = document.createElement("span");
|
||||
value.className = "compose-artifact-score";
|
||||
value.textContent = String(fragment.value || "-");
|
||||
head.appendChild(icon);
|
||||
head.appendChild(title);
|
||||
head.appendChild(value);
|
||||
artifact.appendChild(head);
|
||||
wrap.appendChild(artifact);
|
||||
});
|
||||
row.appendChild(wrap);
|
||||
};
|
||||
|
||||
appendGapArtifacts(msg.gap_fragments);
|
||||
row.dataset.minute = minuteBucketFromTs(msg.ts || 0);
|
||||
appendLatencyChip(row, msg);
|
||||
|
||||
const bubble = document.createElement("article");
|
||||
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
|
||||
@@ -963,13 +1236,51 @@
|
||||
bubble.appendChild(meta);
|
||||
|
||||
row.appendChild(bubble);
|
||||
appendMetricArtifacts(msg.metric_fragments);
|
||||
const empty = thread.querySelector(".compose-empty");
|
||||
if (empty) {
|
||||
empty.remove();
|
||||
}
|
||||
thread.appendChild(row);
|
||||
wireImageFallbacks(row);
|
||||
updateGlanceFromMessage(msg);
|
||||
};
|
||||
|
||||
const applyMinuteGrouping = function () {
|
||||
const rows = Array.from(thread.querySelectorAll(".compose-row"));
|
||||
rows.forEach(function (row) {
|
||||
row.classList.remove(
|
||||
"is-group-single",
|
||||
"is-group-first",
|
||||
"is-group-middle",
|
||||
"is-group-last"
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < rows.length; i += 1) {
|
||||
const row = rows[i];
|
||||
const minute = String(row.dataset.minute || minuteBucketFromTs(row.dataset.ts));
|
||||
const side = row.classList.contains("is-out") ? "out" : "in";
|
||||
const prev = rows[i - 1] || null;
|
||||
const next = rows[i + 1] || null;
|
||||
const prevMatch = !!(
|
||||
prev
|
||||
&& (prev.classList.contains("is-out") ? "out" : "in") === side
|
||||
&& String(prev.dataset.minute || minuteBucketFromTs(prev.dataset.ts)) === minute
|
||||
);
|
||||
const nextMatch = !!(
|
||||
next
|
||||
&& (next.classList.contains("is-out") ? "out" : "in") === side
|
||||
&& String(next.dataset.minute || minuteBucketFromTs(next.dataset.ts)) === minute
|
||||
);
|
||||
if (prevMatch && nextMatch) {
|
||||
row.classList.add("is-group-middle");
|
||||
} else if (!prevMatch && nextMatch) {
|
||||
row.classList.add("is-group-first");
|
||||
} else if (prevMatch && !nextMatch) {
|
||||
row.classList.add("is-group-last");
|
||||
} else {
|
||||
row.classList.add("is-group-single");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const appendMessages = function (messages, forceScroll) {
|
||||
@@ -980,6 +1291,7 @@
|
||||
});
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
if ((messages || []).length > 0) {
|
||||
applyMinuteGrouping();
|
||||
scrollToBottom(shouldStick);
|
||||
}
|
||||
};
|
||||
@@ -1106,6 +1418,7 @@
|
||||
} catch (err) {
|
||||
// Ignore invalid initial typing state payload.
|
||||
}
|
||||
applyMinuteGrouping();
|
||||
|
||||
const setStatus = function (message, level) {
|
||||
if (!statusBox) {
|
||||
@@ -1132,8 +1445,15 @@
|
||||
};
|
||||
|
||||
const setActiveTrigger = function (kind) {
|
||||
const popoverVisible = !!(popover && !popover.classList.contains("is-hidden"));
|
||||
const activeCardVisible = !!(
|
||||
popover
|
||||
&& kind
|
||||
&& popover.querySelector('.compose-ai-card[data-kind="' + kind + '"].is-active')
|
||||
);
|
||||
triggerButtons.forEach(function (button) {
|
||||
button.classList.toggle("is-expanded", !!kind && button.dataset.kind === kind);
|
||||
const expanded = popoverVisible && activeCardVisible && button.dataset.kind === kind;
|
||||
button.classList.toggle("is-expanded", expanded);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1170,6 +1490,7 @@
|
||||
});
|
||||
panelState.activePanel = kind;
|
||||
setActiveTrigger(kind);
|
||||
positionPopover(kind);
|
||||
return active;
|
||||
};
|
||||
|
||||
@@ -1670,6 +1991,13 @@
|
||||
hideAllCards();
|
||||
});
|
||||
}
|
||||
panelState.resizeHandler = function () {
|
||||
if (!popover || popover.classList.contains("is-hidden")) {
|
||||
return;
|
||||
}
|
||||
positionPopover(panelState.activePanel);
|
||||
};
|
||||
window.addEventListener("resize", panelState.resizeHandler);
|
||||
document.addEventListener("mousedown", panelState.docClickHandler);
|
||||
textarea.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
@@ -1686,13 +2014,22 @@
|
||||
textarea.focus();
|
||||
});
|
||||
|
||||
panelState.eventHandler = function () {
|
||||
panelState.eventHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (!sourcePanelId || sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
poll(true);
|
||||
};
|
||||
document.body.addEventListener("composeMessageSent", panelState.eventHandler);
|
||||
|
||||
panelState.sendResultHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (!sourcePanelId || sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
const ok = !!detail.ok;
|
||||
if (ok) {
|
||||
flashCompose("is-send-success");
|
||||
|
||||
Reference in New Issue
Block a user