Implement more information displays
This commit is contained in:
@@ -26,6 +26,10 @@
|
||||
<span class="icon is-small"><i class="fa-solid fa-handshake"></i></span>
|
||||
<span>Engage</span>
|
||||
</button>
|
||||
<button type="button" class="button is-light is-rounded js-ai-trigger" data-kind="quick_insights">
|
||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||
<span>Quick Insights</span>
|
||||
</button>
|
||||
<a class="button is-light is-rounded" href="{{ ai_workspace_url }}">
|
||||
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
|
||||
<span>AI Workspace</span>
|
||||
@@ -57,6 +61,15 @@
|
||||
</div>
|
||||
<div class="compose-ai-content"></div>
|
||||
</div>
|
||||
<div class="compose-ai-card" data-kind="quick_insights">
|
||||
<p class="compose-ai-title">Quick Insights</p>
|
||||
<div class="compose-ai-loading">
|
||||
<div class="compose-ai-skel"></div>
|
||||
<div class="compose-ai-skel"></div>
|
||||
<div class="compose-ai-skel"></div>
|
||||
</div>
|
||||
<div class="compose-ai-content"></div>
|
||||
</div>
|
||||
<div class="compose-ai-card" data-kind="engage">
|
||||
<p class="compose-ai-title">Quick Engage (Shared Framing)</p>
|
||||
<div class="compose-engage-source-row">
|
||||
@@ -103,6 +116,7 @@
|
||||
data-ws-url="{{ compose_ws_url }}"
|
||||
data-drafts-url="{{ compose_drafts_url }}"
|
||||
data-summary-url="{{ compose_summary_url }}"
|
||||
data-quick-insights-url="{{ compose_quick_insights_url }}"
|
||||
data-engage-preview-url="{{ compose_engage_preview_url }}"
|
||||
data-engage-send-url="{{ compose_engage_send_url }}">
|
||||
{% for msg in serialized_messages %}
|
||||
@@ -381,19 +395,25 @@
|
||||
margin-bottom: 0.45rem;
|
||||
border-radius: 8px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
line-height: 1.35;
|
||||
padding: 0.58rem 0.62rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-draft-option strong {
|
||||
display: inline;
|
||||
margin-right: 0.2rem;
|
||||
white-space: normal;
|
||||
#{{ panel_id }} .compose-draft-tone {
|
||||
margin: 0 0 0.2rem 0;
|
||||
color: #6e7782;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#{{ panel_id }} .compose-draft-option span {
|
||||
white-space: normal;
|
||||
#{{ panel_id }} .compose-draft-text {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -406,11 +426,12 @@
|
||||
#{{ panel_id }} .js-ai-trigger {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
#{{ panel_id }} .js-ai-trigger.is-expanded {
|
||||
background: #eef6ff;
|
||||
border-color: #8bb2e6;
|
||||
}
|
||||
#{{ panel_id }} .js-ai-trigger.is-expanded {
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .js-ai-trigger.is-expanded::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -424,6 +445,83 @@
|
||||
border-top: 6px solid #8bb2e6;
|
||||
pointer-events: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-head {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.32rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-chip {
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 0.35rem 0.42rem;
|
||||
background: #fff;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-chip p {
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-chip .k {
|
||||
color: #657283;
|
||||
font-size: 0.67rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-chip .v {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-list {
|
||||
display: grid;
|
||||
gap: 0.36rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row {
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 0.42rem 0.46rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.32rem;
|
||||
min-width: 0;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.44rem;
|
||||
font-size: 0.68rem;
|
||||
color: #657283;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-row-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #202835;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-docs {
|
||||
margin: 0.5rem 0 0;
|
||||
padding-left: 1.1rem;
|
||||
color: #657283;
|
||||
font-size: 0.71rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-qi-docs li {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-image-fallback.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -515,6 +613,9 @@
|
||||
if (previousState && previousState.eventHandler) {
|
||||
document.body.removeEventListener("composeMessageSent", previousState.eventHandler);
|
||||
}
|
||||
if (previousState && previousState.sendResultHandler) {
|
||||
document.body.removeEventListener("composeSendResult", previousState.sendResultHandler);
|
||||
}
|
||||
if (previousState && previousState.docClickHandler) {
|
||||
document.removeEventListener("mousedown", previousState.docClickHandler);
|
||||
}
|
||||
@@ -554,6 +655,37 @@
|
||||
}
|
||||
};
|
||||
|
||||
const extractUrlCandidates = function (value) {
|
||||
const raw = String(value || "");
|
||||
const matches = raw.match(/https?:\/\/[^\s<>'"\\]+/g) || [];
|
||||
const seen = new Set();
|
||||
const output = [];
|
||||
matches.forEach(function (item) {
|
||||
const cleaned = String(item).trim().replace(/[.,);:!?\"']+$/g, "");
|
||||
if (!cleaned || seen.has(cleaned)) {
|
||||
return;
|
||||
}
|
||||
seen.add(cleaned);
|
||||
output.push(cleaned);
|
||||
});
|
||||
return output;
|
||||
};
|
||||
|
||||
const appendImageCandidates = function (bubble, candidates) {
|
||||
(candidates || []).forEach(function (candidateUrl) {
|
||||
const figure = document.createElement("figure");
|
||||
figure.className = "compose-media";
|
||||
const img = document.createElement("img");
|
||||
img.className = "compose-image";
|
||||
img.src = String(candidateUrl);
|
||||
img.alt = "Attachment";
|
||||
img.loading = "lazy";
|
||||
img.decoding = "async";
|
||||
figure.appendChild(img);
|
||||
bubble.insertBefore(figure, bubble.firstChild);
|
||||
});
|
||||
};
|
||||
|
||||
const wireImageFallbacks = function (rootNode) {
|
||||
const scope = rootNode || thread;
|
||||
if (!scope) {
|
||||
@@ -590,6 +722,28 @@
|
||||
});
|
||||
};
|
||||
|
||||
const hydrateBodyUrlsAsImages = function (rootNode) {
|
||||
const scope = rootNode || thread;
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
scope.querySelectorAll(".compose-bubble").forEach(function (bubble) {
|
||||
if (bubble.querySelector(".compose-image")) {
|
||||
return;
|
||||
}
|
||||
const body = bubble.querySelector(".compose-body");
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const candidates = extractUrlCandidates(body.textContent || "");
|
||||
if (!candidates.length) {
|
||||
return;
|
||||
}
|
||||
appendImageCandidates(bubble, candidates);
|
||||
});
|
||||
wireImageFallbacks(scope);
|
||||
};
|
||||
|
||||
const appendBubble = function (msg) {
|
||||
const row = document.createElement("div");
|
||||
const outgoing = !!msg.outgoing;
|
||||
@@ -599,21 +753,13 @@
|
||||
const bubble = document.createElement("article");
|
||||
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
|
||||
|
||||
const imageCandidates = Array.isArray(msg.image_urls) && msg.image_urls.length
|
||||
const imageCandidatesFromPayload = Array.isArray(msg.image_urls) && msg.image_urls.length
|
||||
? msg.image_urls
|
||||
: (msg.image_url ? [msg.image_url] : []);
|
||||
imageCandidates.forEach(function (candidateUrl) {
|
||||
const figure = document.createElement("figure");
|
||||
figure.className = "compose-media";
|
||||
const img = document.createElement("img");
|
||||
img.className = "compose-image";
|
||||
img.src = String(candidateUrl);
|
||||
img.alt = "Attachment";
|
||||
img.loading = "lazy";
|
||||
img.decoding = "async";
|
||||
figure.appendChild(img);
|
||||
bubble.appendChild(figure);
|
||||
});
|
||||
const imageCandidates = imageCandidatesFromPayload.length
|
||||
? imageCandidatesFromPayload
|
||||
: extractUrlCandidates(msg.text || msg.display_text || "");
|
||||
appendImageCandidates(bubble, imageCandidates);
|
||||
|
||||
if (!msg.hide_text) {
|
||||
const body = document.createElement("p");
|
||||
@@ -908,11 +1054,13 @@
|
||||
const engageButton = document.createElement("button");
|
||||
engageButton.type = "button";
|
||||
engageButton.className = "button is-link is-light compose-draft-option";
|
||||
const engageStrong = document.createElement("strong");
|
||||
engageStrong.textContent = "Custom Engage: ";
|
||||
const engageText = document.createElement("span");
|
||||
const engageTone = document.createElement("p");
|
||||
engageTone.className = "compose-draft-tone";
|
||||
engageTone.textContent = "Custom Engage";
|
||||
const engageText = document.createElement("p");
|
||||
engageText.className = "compose-draft-text";
|
||||
engageText.textContent = "Choose a source or write your own engagement text.";
|
||||
engageButton.appendChild(engageStrong);
|
||||
engageButton.appendChild(engageTone);
|
||||
engageButton.appendChild(engageText);
|
||||
engageButton.addEventListener("click", function () {
|
||||
openEngage("custom");
|
||||
@@ -922,12 +1070,14 @@
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "button is-light compose-draft-option";
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = String(item.label || "Option") + ": ";
|
||||
const span = document.createElement("span");
|
||||
span.textContent = String(item.text || "");
|
||||
button.appendChild(strong);
|
||||
button.appendChild(span);
|
||||
const tone = document.createElement("p");
|
||||
tone.className = "compose-draft-tone";
|
||||
tone.textContent = String(item.label || "Option");
|
||||
const body = document.createElement("p");
|
||||
body.className = "compose-draft-text";
|
||||
body.textContent = String(item.text || "");
|
||||
button.appendChild(tone);
|
||||
button.appendChild(body);
|
||||
button.addEventListener("click", function () {
|
||||
textarea.value = String(item.text || "");
|
||||
autosize();
|
||||
@@ -967,6 +1117,110 @@
|
||||
}
|
||||
};
|
||||
|
||||
const loadQuickInsights = async function () {
|
||||
const card = showCard("quick_insights");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
setCardLoading(card, true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
thread.dataset.quickInsightsUrl + "?" + queryParams().toString(),
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/json" }
|
||||
}
|
||||
);
|
||||
const payload = await response.json();
|
||||
setCardLoading(card, false);
|
||||
const container = card.querySelector(".compose-ai-content");
|
||||
if (!payload.ok) {
|
||||
container.textContent = payload.error || "Failed to load quick insights.";
|
||||
return;
|
||||
}
|
||||
const summary = payload.summary || {};
|
||||
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
||||
const docs = Array.isArray(payload.docs) ? payload.docs : [];
|
||||
container.innerHTML = "";
|
||||
|
||||
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 || "-"],
|
||||
].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>"
|
||||
);
|
||||
head.appendChild(chip);
|
||||
});
|
||||
container.appendChild(head);
|
||||
|
||||
if (!rows.length) {
|
||||
const none = document.createElement("p");
|
||||
none.className = "is-size-7 has-text-grey";
|
||||
none.textContent = "No metric rows available yet.";
|
||||
container.appendChild(none);
|
||||
} else {
|
||||
const list = document.createElement("div");
|
||||
list.className = "compose-qi-list";
|
||||
rows.forEach(function (row) {
|
||||
const node = document.createElement("article");
|
||||
node.className = "compose-qi-row";
|
||||
node.innerHTML = (
|
||||
'<div class="compose-qi-row-head">'
|
||||
+ '<p class="compose-qi-row-label"><span class="icon is-small"><i class="'
|
||||
+ String(row.icon || "fa-solid fa-square") + '"></i></span><span>'
|
||||
+ String(row.label || "") + "</span></p>"
|
||||
+ '<p class="compose-qi-row-meta"><span>' + String(row.point_count || 0)
|
||||
+ ' points</span><span class="' + String((row.trend || {}).class_name || "")
|
||||
+ '"><span class="icon is-small"><i class="' + String((row.trend || {}).icon || "")
|
||||
+ '"></i></span> ' + String(row.delta_label || "n/a")
|
||||
+ "</span></p></div>"
|
||||
+ '<div class="compose-qi-row-body">'
|
||||
+ '<p class="compose-qi-value">' + String(row.display_value || "-") + "</p>"
|
||||
+ '<p class="' + String(((row.emotion || {}).class_name) || "")
|
||||
+ '" style="margin:0; font-size:0.72rem;">'
|
||||
+ '<span class="icon is-small"><i class="' + String(((row.emotion || {}).icon) || "")
|
||||
+ '"></i></span> ' + String(((row.emotion || {}).label) || "Unknown")
|
||||
+ "</p></div>"
|
||||
);
|
||||
list.appendChild(node);
|
||||
});
|
||||
container.appendChild(list);
|
||||
}
|
||||
if (docs.length) {
|
||||
const docsList = document.createElement("ul");
|
||||
docsList.className = "compose-qi-docs";
|
||||
docs.forEach(function (item) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = String(item || "");
|
||||
docsList.appendChild(li);
|
||||
});
|
||||
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 =
|
||||
"Failed to load quick insights.";
|
||||
}
|
||||
};
|
||||
|
||||
const loadEngage = async function (card, preferredSource) {
|
||||
card = card || showCard("engage");
|
||||
if (!card) {
|
||||
@@ -1157,6 +1411,8 @@
|
||||
loadDrafts();
|
||||
} else if (kind === "summary") {
|
||||
loadSummary();
|
||||
} else if (kind === "quick_insights") {
|
||||
loadQuickInsights();
|
||||
} else if (kind === "engage") {
|
||||
openEngage("auto");
|
||||
}
|
||||
@@ -1184,7 +1440,6 @@
|
||||
});
|
||||
}
|
||||
document.addEventListener("mousedown", panelState.docClickHandler);
|
||||
|
||||
textarea.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
@@ -1224,7 +1479,7 @@
|
||||
};
|
||||
document.body.addEventListener("composeSendResult", panelState.sendResultHandler);
|
||||
|
||||
wireImageFallbacks(thread);
|
||||
hydrateBodyUrlsAsImages(thread);
|
||||
scrollToBottom(true);
|
||||
setupWebSocket();
|
||||
panelState.timer = setInterval(function () {
|
||||
|
||||
Reference in New Issue
Block a user