Further improve detail display and work on inline latency display

This commit is contained in:
2026-02-15 20:08:02 +00:00
parent 1ebd565f44
commit 63af5d234e
17 changed files with 1223 additions and 239 deletions

View File

@@ -121,6 +121,25 @@
data-engage-send-url="{{ compose_engage_send_url }}">
{% 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>
{% endif %}
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
{% if msg.image_urls %}
{% for image_url in msg.image_urls %}
@@ -152,6 +171,21 @@
{{ 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>
@@ -219,13 +253,15 @@
}
#{{ panel_id }} .compose-row {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-bottom: 0.5rem;
}
#{{ panel_id }} .compose-row.is-in {
justify-content: flex-start;
align-items: flex-start;
}
#{{ panel_id }} .compose-row.is-out {
justify-content: flex-end;
align-items: flex-end;
}
#{{ panel_id }} .compose-bubble {
max-width: min(85%, 46rem);
@@ -265,6 +301,49 @@
#{{ panel_id }} .compose-msg-meta {
margin: 0;
}
#{{ panel_id }} .compose-gap-artifacts {
align-self: center;
width: min(92%, 34rem);
}
#{{ panel_id }} .compose-metric-artifacts {
width: min(86%, 46rem);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9.4rem, 1fr));
gap: 0.28rem;
}
#{{ panel_id }} .compose-artifact {
border: 1px dashed rgba(0, 0, 0, 0.16);
border-radius: 8px;
background: rgba(252, 253, 255, 0.96);
padding: 0.28rem 0.38rem;
}
#{{ panel_id }} .compose-artifact.compose-artifact-gap {
margin-bottom: 0.2rem;
}
#{{ panel_id }} .compose-artifact-head {
margin: 0;
display: flex;
gap: 0.3rem;
align-items: center;
color: #3f4f67;
font-size: 0.68rem;
line-height: 1.25;
}
#{{ panel_id }} .compose-artifact-head .icon {
color: #6a88b4;
}
#{{ panel_id }} .compose-artifact-score {
margin-left: auto;
color: #2f4f7a;
font-weight: 700;
font-size: 0.66rem;
}
#{{ panel_id }} .compose-artifact-detail {
margin: 0.15rem 0 0;
color: #637185;
font-size: 0.64rem;
line-height: 1.25;
}
#{{ panel_id }} .compose-empty {
margin: 0;
color: #6f6f6f;
@@ -456,6 +535,7 @@
border-radius: 8px;
padding: 0.35rem 0.42rem;
background: #fff;
min-width: 0;
}
#{{ panel_id }} .compose-qi-chip p {
margin: 0;
@@ -468,9 +548,22 @@
#{{ panel_id }} .compose-qi-chip .v {
font-size: 0.78rem;
font-weight: 600;
display: flex;
align-items: flex-start;
min-width: 0;
gap: 0.28rem;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
#{{ panel_id }} .compose-qi-chip .v > span:last-child {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-all;
}
#{{ panel_id }} .compose-qi-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.36rem;
}
#{{ panel_id }} .compose-qi-row {
@@ -478,6 +571,7 @@
border-radius: 8px;
background: #fff;
padding: 0.42rem 0.46rem;
height: 100%;
}
#{{ panel_id }} .compose-qi-row-head {
display: flex;
@@ -750,6 +844,88 @@
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);
const bubble = document.createElement("article");
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
@@ -787,6 +963,7 @@
bubble.appendChild(meta);
row.appendChild(bubble);
appendMetricArtifacts(msg.metric_fragments);
const empty = thread.querySelector(".compose-empty");
if (empty) {
empty.remove();
@@ -1144,20 +1321,83 @@
const docs = Array.isArray(payload.docs) ? payload.docs : [];
container.innerHTML = "";
const stateFaceMeta = function (stateText) {
const state = String(stateText || "").toLowerCase();
if (state.includes("balanced")) {
return {
icon: "fa-regular fa-face-smile",
className: "has-text-success",
label: "Balanced"
};
}
if (state.includes("withdrawing")) {
return {
icon: "fa-regular fa-face-frown",
className: "has-text-danger",
label: "Withdrawing"
};
}
if (state.includes("overextending")) {
return {
icon: "fa-regular fa-face-meh",
className: "has-text-warning",
label: "Overextending"
};
}
if (state.includes("stable")) {
return {
icon: "fa-regular fa-face-smile",
className: "has-text-success",
label: "Positive"
};
}
if (state.includes("watch")) {
return {
icon: "fa-regular fa-face-meh",
className: "has-text-warning",
label: "Mixed"
};
}
if (state.includes("fragile")) {
return {
icon: "fa-regular fa-face-frown",
className: "has-text-danger",
label: "Strained"
};
}
return {
icon: "fa-regular fa-face-meh-blank",
className: "has-text-grey",
label: "Unknown"
};
};
const stateFace = stateFaceMeta(summary.state);
const head = document.createElement("div");
head.className = "compose-qi-head";
[
["Platform", summary.platform || "-"],
["State", summary.state || "-"],
["Data Points", String(summary.snapshot_count || 0)],
["Thread", summary.thread || "-"],
{ key: "Platform", value: summary.platform || "-" },
{
key: "Participant State",
value: summary.state || "-",
icon: stateFace.icon,
className: stateFace.className,
},
{ key: "Data Points", value: String(summary.snapshot_count || 0) },
{ key: "Thread", value: summary.thread || "-" },
].forEach(function (pair) {
const chip = document.createElement("div");
chip.className = "compose-qi-chip";
chip.innerHTML = (
'<p class="k">' + pair[0] + "</p>"
+ '<p class="v">' + pair[1] + "</p>"
);
let valueHtml = String(pair.value || "-");
if (pair.icon) {
valueHtml = (
'<span class="' + String(pair.className || "") + '">'
+ '<span class="icon is-small"><i class="' + String(pair.icon) + '"></i></span>'
+ "</span>"
+ "<span>" + valueHtml + "</span>"
);
}
chip.innerHTML = '<p class="k">' + pair.key + "</p>" + '<p class="v">' + valueHtml + "</p>";
head.appendChild(chip);
});
container.appendChild(head);
@@ -1205,15 +1445,6 @@
});
container.appendChild(docsList);
}
const openBtn = document.createElement("a");
openBtn.className = "button is-light is-small is-rounded";
openBtn.href = "{{ ai_workspace_url }}";
openBtn.innerHTML = (
'<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>'
+ "<span>Open Minimal AI Workspace</span>"
);
openBtn.style.marginTop = "0.45rem";
container.appendChild(openBtn);
} catch (err) {
setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent =