Work on fixing bugs and reformat

This commit is contained in:
2026-02-16 16:01:17 +00:00
parent 8ca1695fab
commit 3f82c27ab9
32 changed files with 1100 additions and 442 deletions

View File

@@ -399,8 +399,8 @@
return String(value || "")
.split(",")
.map(function (item) {
return item.trim();
})
return item.trim();
})
.filter(Boolean);
};

View File

@@ -536,8 +536,8 @@
showOperationPane(operation);
const activeTab = tabKey || (
operation === "artifacts"
? ((window.giaWorkspaceState[personId] || {}).currentMitigationTab || "plan_board")
: operation
? ((window.giaWorkspaceState[personId] || {}).currentMitigationTab || "plan_board")
: operation
);
setTopCapsuleActive(activeTab);
const hydrated = hydrateCachedIfAvailable(operation);
@@ -573,8 +573,8 @@
const currentState = window.giaWorkspaceState[personId] || {};
const targetTabKey = currentState.pendingTabKey || (
operation === "artifacts"
? (currentState.currentMitigationTab || "plan_board")
: operation
? (currentState.currentMitigationTab || "plan_board")
: operation
);
if (!forceRefresh && currentState.current === operation && pane.dataset.loaded === "1") {
window.giaWorkspaceShowTab(personId, operation, targetTabKey);
@@ -663,8 +663,8 @@
const state = window.giaWorkspaceState[personId] || {};
const currentTab = state.currentTab || (
state.current === "artifacts"
? (state.currentMitigationTab || "plan_board")
: (state.current || "plan_board")
? (state.currentMitigationTab || "plan_board")
: (state.current || "plan_board")
);
window.giaWorkspaceOpenTab(personId, currentTab, true);
};

View File

@@ -254,6 +254,9 @@
{% endwith %}
{% endif %}
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
<div class="compose-source-badge-wrap">
<span class="compose-source-badge source-{{ msg.source_service|default:'web'|lower }}">{{ msg.source_label }}</span>
</div>
{% if msg.image_urls %}
{% for image_url in msg.image_urls %}
<figure class="compose-media">
@@ -285,14 +288,30 @@
<p class="compose-msg-meta">
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
{% if msg.read_ts %}
<span class="compose-ticks" title="Read at {{ msg.read_display }}">
<span
class="compose-ticks js-receipt-trigger"
role="button"
tabindex="0"
data-receipt='{{ msg.receipt_payload|default:"{}"|escapejs }}'
data-source='{{ msg.read_source_service }}'
data-by='{{ msg.read_by_identifier }}'
data-id='{{ msg.id }}'
title="Read at {{ msg.read_display }}">
<span class="icon is-small"><i class="fa-solid fa-check-double has-text-info"></i></span>
<span class="compose-tick-time">{{ msg.read_display }}</span>
<span class="compose-tick-time">{{ msg.read_delta_display }}</span>
</span>
{% elif msg.delivered_ts %}
<span class="compose-ticks" title="Delivered at {{ msg.delivered_display }}">
<span
class="compose-ticks js-receipt-trigger"
role="button"
tabindex="0"
data-receipt='{{ msg.receipt_payload|default:"{}"|escapejs }}'
data-source='{{ msg.read_source_service }}'
data-by='{{ msg.read_by_identifier }}'
data-id='{{ msg.id }}'
title="Delivered at {{ msg.delivered_display }}">
<span class="icon is-small"><i class="fa-solid fa-check-double has-text-grey"></i></span>
<span class="compose-tick-time">{{ msg.delivered_display }}</span>
<span class="compose-tick-time">{{ msg.delivered_delta_display }}</span>
</span>
{% endif %}
</p>
@@ -438,6 +457,26 @@
padding: 0.52rem 0.62rem;
box-shadow: none;
}
#{{ panel_id }} .compose-source-badge-wrap {
display: flex;
justify-content: flex-start;
margin-bottom: 0.36rem;
}
#{{ panel_id }} .compose-source-badge {
font-size: 0.84rem;
padding: 0.12rem 0.5rem;
border-radius: 6px;
color: #fff;
font-weight: 800;
letter-spacing: 0.02em;
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
}
#{{ panel_id }} .compose-source-badge.source-web { background: #2f4f7a; }
#{{ panel_id }} .compose-source-badge.source-xmpp { background: #6a88b4; }
#{{ panel_id }} .compose-source-badge.source-whatsapp { background: #25D366; color: #063; }
#{{ panel_id }} .compose-source-badge.source-signal { background: #3b82f6; }
#{{ panel_id }} .compose-source-badge.source-instagram { background: #c13584; }
#{{ panel_id }} .compose-source-badge.source-unknown { background: #6b7280; }
#{{ panel_id }} .compose-bubble.is-in {
background: rgba(255, 255, 255, 0.96);
}
@@ -1719,6 +1758,7 @@
};
const appendBubble = function (msg) {
console.log("[appendBubble]", {id: msg.id, ts: msg.ts, author: msg.author, source_label: msg.source_label, source_service: msg.source_service, outgoing: msg.outgoing});
const row = document.createElement("div");
const outgoing = !!msg.outgoing;
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
@@ -1729,12 +1769,24 @@
const bubble = document.createElement("article");
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
// Add source badge for client-side rendered messages
if (msg.source_label) {
console.log("[appendBubble] rendering source badge:", msg.source_label);
const badgeWrap = document.createElement("div");
badgeWrap.className = "compose-source-badge-wrap";
const badge = document.createElement("span");
const svc = String(msg.source_service || "web").toLowerCase();
badge.className = "compose-source-badge source-" + svc;
badge.textContent = String(msg.source_label || "");
badgeWrap.appendChild(badge);
bubble.appendChild(badgeWrap);
}
const imageCandidatesFromPayload = Array.isArray(msg.image_urls) && msg.image_urls.length
? msg.image_urls
: (msg.image_url ? [msg.image_url] : []);
? msg.image_urls
: (msg.image_url ? [msg.image_url] : []);
const imageCandidates = imageCandidatesFromPayload.length
? imageCandidatesFromPayload
: extractUrlCandidates(msg.text || msg.display_text || "");
? imageCandidatesFromPayload
: extractUrlCandidates(msg.text || msg.display_text || "");
appendImageCandidates(bubble, imageCandidates);
if (!msg.hide_text) {
@@ -1759,44 +1811,55 @@
if (msg.author) {
metaText += " · " + String(msg.author);
}
meta.textContent = metaText;
// Render delivery/read ticks and a small time label when available.
if (msg.read_ts) {
const tickWrap = document.createElement("span");
tickWrap.className = "compose-ticks";
tickWrap.title = "Read at " + String(msg.read_display || msg.read_ts || "");
const icon = document.createElement("span");
icon.className = "icon is-small";
const i = document.createElement("i");
i.className = "fa-solid fa-check-double has-text-info";
icon.appendChild(i);
const timeSpan = document.createElement("span");
timeSpan.className = "compose-tick-time";
timeSpan.textContent = String(msg.read_display || "");
tickWrap.appendChild(icon);
tickWrap.appendChild(timeSpan);
meta.appendChild(document.createTextNode(" "));
meta.appendChild(tickWrap);
} else if (msg.delivered_ts) {
const tickWrap = document.createElement("span");
tickWrap.className = "compose-ticks";
tickWrap.title = "Delivered at " + String(msg.delivered_display || msg.delivered_ts || "");
const icon = document.createElement("span");
icon.className = "icon is-small";
const i = document.createElement("i");
i.className = "fa-solid fa-check-double has-text-grey";
icon.appendChild(i);
const timeSpan = document.createElement("span");
timeSpan.className = "compose-tick-time";
timeSpan.textContent = String(msg.delivered_display || "");
tickWrap.appendChild(icon);
tickWrap.appendChild(timeSpan);
meta.appendChild(document.createTextNode(" "));
meta.appendChild(tickWrap);
}
meta.textContent = metaText;
// Render delivery/read ticks and a small time label when available.
if (msg.read_ts) {
const tickWrap = document.createElement("span");
tickWrap.className = "compose-ticks";
tickWrap.title = "Read at " + String(msg.read_display || msg.read_ts || "");
const icon = document.createElement("span");
icon.className = "icon is-small";
const i = document.createElement("i");
i.className = "fa-solid fa-check-double has-text-info";
icon.appendChild(i);
const timeSpan = document.createElement("span");
timeSpan.className = "compose-tick-time";
timeSpan.textContent = String(msg.read_display || "");
tickWrap.appendChild(icon);
tickWrap.appendChild(timeSpan);
meta.appendChild(document.createTextNode(" "));
meta.appendChild(tickWrap);
} else if (msg.delivered_ts) {
const tickWrap = document.createElement("span");
tickWrap.className = "compose-ticks";
tickWrap.title = "Delivered at " + String(msg.delivered_display || msg.delivered_ts || "");
const icon = document.createElement("span");
icon.className = "icon is-small";
const i = document.createElement("i");
i.className = "fa-solid fa-check-double has-text-grey";
icon.appendChild(i);
const timeSpan = document.createElement("span");
timeSpan.className = "compose-tick-time";
timeSpan.textContent = String(msg.delivered_display || "");
tickWrap.appendChild(icon);
tickWrap.appendChild(timeSpan);
meta.appendChild(document.createTextNode(" "));
meta.appendChild(tickWrap);
}
bubble.appendChild(meta);
row.appendChild(bubble);
// If message carries receipt metadata, append dataset so the popover can use it.
if (msg.receipt_payload || msg.read_source_service || msg.read_by_identifier) {
// Attach data attributes on the row so event delegation can find them.
try {
row.dataset.receipt = JSON.stringify(msg.receipt_payload || {});
} catch (e) {
row.dataset.receipt = "{}";
}
row.dataset.receiptSource = String(msg.read_source_service || "");
row.dataset.receiptBy = String(msg.read_by_identifier || "");
row.dataset.receiptId = String(msg.id || "");
}
const empty = thread.querySelector(".compose-empty");
if (empty) {
empty.remove();
@@ -1810,6 +1873,87 @@
updateGlanceFromMessage(msg);
};
// Receipt popover (similar to contact info popover)
const receiptPopover = document.createElement("div");
receiptPopover.id = "compose-receipt-popover";
receiptPopover.className = "compose-ai-popover is-hidden";
receiptPopover.setAttribute("aria-hidden", "true");
receiptPopover.innerHTML = `
<div class="compose-ai-card is-active" style="min-width:18rem;">
<p class="compose-ai-title">Receipt Details</p>
<div class="compose-ai-content">
<table class="table is-fullwidth is-striped is-size-7"><tbody>
<tr><th>Message ID</th><td id="receipt-msg-id">-</td></tr>
<tr><th>Source</th><td id="receipt-source">-</td></tr>
<tr><th>Read By</th><td id="receipt-by">-</td></tr>
<tr><th>Delivered</th><td id="receipt-delivered">-</td></tr>
<tr><th>Read</th><td id="receipt-read">-</td></tr>
<tr><th>Payload</th><td><pre id="receipt-payload" style="white-space:pre-wrap;max-height:18rem;overflow:auto"></pre></td></tr>
</tbody></table>
</div>
</div>
`;
document.body.appendChild(receiptPopover);
let activeReceiptBtn = null;
function hideReceiptPopover() {
receiptPopover.classList.add("is-hidden");
receiptPopover.setAttribute("aria-hidden", "true");
activeReceiptBtn = null;
}
function positionReceiptPopover(btn) {
const rect = btn.getBoundingClientRect();
const width = Math.min(520, Math.max(280, Math.floor(window.innerWidth * 0.32)));
const left = Math.min(window.innerWidth - width - 16, Math.max(12, rect.left - width + rect.width));
const top = Math.min(window.innerHeight - 24, rect.bottom + 8);
receiptPopover.style.left = left + "px";
receiptPopover.style.top = top + "px";
receiptPopover.style.width = width + "px";
}
function openReceiptPopoverFromData(data, btn) {
document.getElementById("receipt-msg-id").textContent = data.id || "-";
document.getElementById("receipt-source").textContent = data.source || "-";
document.getElementById("receipt-by").textContent = data.by || "-";
document.getElementById("receipt-delivered").textContent = data.delivered || "-";
document.getElementById("receipt-read").textContent = data.read || "-";
try {
const out = typeof data.payload === 'string' ? JSON.parse(data.payload) : data.payload || {};
document.getElementById("receipt-payload").textContent = JSON.stringify(out, null, 2);
} catch (e) {
document.getElementById("receipt-payload").textContent = String(data.payload || "{}");
}
positionReceiptPopover(btn);
receiptPopover.classList.remove("is-hidden");
receiptPopover.setAttribute("aria-hidden", "false");
}
// Delegate click on tick triggers inside thread
thread.addEventListener("click", function (ev) {
const btn = ev.target.closest && ev.target.closest('.js-receipt-trigger');
if (!btn) return;
if (activeReceiptBtn === btn && !receiptPopover.classList.contains('is-hidden')) {
hideReceiptPopover();
return;
}
activeReceiptBtn = btn;
const payload = btn.dataset && btn.dataset.receipt ? btn.dataset.receipt : (btn.parentNode && btn.parentNode.dataset ? btn.parentNode.dataset.receipt : "{}");
const source = btn.dataset && btn.dataset.source ? btn.dataset.source : (btn.parentNode && btn.parentNode.dataset ? btn.parentNode.dataset.receiptSource : "");
const by = btn.dataset && btn.dataset.by ? btn.dataset.by : (btn.parentNode && btn.parentNode.dataset ? btn.parentNode.dataset.receiptBy : "");
const id = btn.dataset && btn.dataset.id ? btn.dataset.id : (btn.parentNode && btn.parentNode.dataset ? btn.parentNode.dataset.receiptId : "");
const delivered = btn.title || "";
const read = btn.title || "";
openReceiptPopoverFromData({ id: id, payload: payload, source: source, by: by, delivered: delivered, read: read }, btn);
});
// Close receipt popover on outside click / escape
document.addEventListener("click", function (ev) {
if (receiptPopover.classList.contains('is-hidden')) return;
if (receiptPopover.contains(ev.target)) return;
if (activeReceiptBtn && activeReceiptBtn.contains(ev.target)) return;
hideReceiptPopover();
});
document.addEventListener("keydown", function (ev) { if (ev.key === 'Escape') hideReceiptPopover(); });
const applyMinuteGrouping = function () {
const rows = Array.from(thread.querySelectorAll(".compose-row"));
rows.forEach(function (row) {
@@ -1889,15 +2033,18 @@
}
params.set("limit", thread.dataset.limit || "60");
params.set("after_ts", String(lastTs));
console.log("[poll] fetching messages: service=" + params.get("service") + " after_ts=" + lastTs);
const response = await fetch(thread.dataset.pollUrl + "?" + params.toString(), {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
if (!response.ok) {
console.log("[poll] response not ok:", response.status);
return;
}
const payload = await response.json();
console.log("[poll] received payload with " + (payload.messages ? payload.messages.length : 0) + " messages");
appendMessages(payload.messages || [], forceScroll);
if (payload.typing) {
applyTyping(payload.typing);
@@ -2522,7 +2669,7 @@
} catch (err) {
setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent =
"Failed to load quick insights.";
"Failed to load quick insights.";
}
};
@@ -2541,8 +2688,8 @@
const customText = card.querySelector(".engage-custom-text");
const selectedSource = (
preferredSource !== undefined
? preferredSource
: (sourceSelect ? sourceSelect.value : "")
? preferredSource
: (sourceSelect ? sourceSelect.value : "")
);
const customValue = customText ? String(customText.value || "").trim() : "";
const showCustom = selectedSource === "custom";
@@ -2734,8 +2881,8 @@
const selectedPerson = selected.dataset.person || thread.dataset.person || "";
const selectedPageUrl = (
renderMode === "page"
? selected.dataset.pageUrl
: selected.dataset.widgetUrl
? selected.dataset.pageUrl
: selected.dataset.widgetUrl
) || "";
switchThreadContext(
selectedService,
@@ -2764,8 +2911,8 @@
const selectedPerson = selected.dataset.person || "";
let selectedPageUrl = (
renderMode === "page"
? selected.dataset[servicePageUrlKey]
: selected.dataset[serviceWidgetUrlKey]
? selected.dataset[servicePageUrlKey]
: selected.dataset[serviceWidgetUrlKey]
) || "";
if (!selectedIdentifier) {
selectedService = selected.dataset.service || selectedService;
@@ -2774,8 +2921,8 @@
if (!selectedPageUrl) {
selectedPageUrl = (
renderMode === "page"
? selected.dataset.pageUrl
: selected.dataset.widgetUrl
? selected.dataset.pageUrl
: selected.dataset.widgetUrl
) || "";
}
switchThreadContext(
@@ -2877,6 +3024,51 @@
textarea.focus();
});
// Cancel send support: show a cancel button while the form request is pending.
let cancelBtn = null;
const showCancelButton = function () {
if (cancelBtn) return;
cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'button is-danger is-light is-small compose-cancel-send-btn';
cancelBtn.textContent = 'Cancel Send';
cancelBtn.addEventListener('click', function () {
// Post cancel by service+identifier
const payload = new URLSearchParams();
payload.set('service', thread.dataset.service || '');
payload.set('identifier', thread.dataset.identifier || '');
fetch('{% url "compose_cancel_send" %}', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': '{{ csrf_token }}', 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload.toString(),
}).then(function (resp) {
// Hide cancel once requested
hideCancelButton();
}).catch(function () {
hideCancelButton();
});
});
if (statusBox) {
statusBox.appendChild(cancelBtn);
}
};
const hideCancelButton = function () {
if (!cancelBtn) return;
try { cancelBtn.remove(); } catch (e) {}
cancelBtn = null;
};
// Show cancel on submit; htmx will make the request asynchronously.
form.addEventListener('submit', function (ev) {
// Only show when send confirmation allows
if (sendButton && sendButton.disabled) return;
showCancelButton();
});
// Hide cancel after HTMX request completes
form.addEventListener('htmx:afterRequest', function () { hideCancelButton(); });
panelState.eventHandler = function (event) {
const detail = (event && event.detail) || {};
const sourcePanelId = String(detail.panel_id || "");
@@ -2887,6 +3079,114 @@
};
document.body.addEventListener("composeMessageSent", panelState.eventHandler);
// Persistent queued-command handling: when server returns composeSendCommandId
// HTMX will dispatch a `composeSendCommandId` event with detail {command_id: "..."}.
panelState.pendingCommandId = null;
panelState.pendingCommandPoll = null;
const startPendingCommandPolling = function (commandId) {
if (!commandId) return;
panelState.pendingCommandId = commandId;
// Show persistent cancel UI
showPersistentCancelButton(commandId);
// Poll for result every 1500ms
if (panelState.pendingCommandPoll) {
clearInterval(panelState.pendingCommandPoll);
}
panelState.pendingCommandPoll = setInterval(async function () {
try {
const url = new URL('{% url "compose_command_result" %}', window.location.origin);
url.searchParams.set('service', thread.dataset.service || '');
url.searchParams.set('command_id', commandId);
const resp = await fetch(url.toString(), { credentials: 'same-origin' });
if (!resp.ok) return;
const payload = await resp.json();
if (payload && payload.pending === false) {
// Stop polling
stopPendingCommandPolling();
// Hide cancel UI
hidePersistentCancelButton();
// Surface result to the user
const result = payload.result || {};
if (result.ok) {
setStatus('', 'success');
textarea.value = '';
autosize();
flashCompose('is-send-success');
poll(true);
} else {
const msg = String(result.error || 'Send failed.');
setStatus(msg, 'danger');
flashCompose('is-send-fail');
poll(true);
}
}
} catch (e) {
// ignore transient network errors
}
}, 1500);
};
const stopPendingCommandPolling = function () {
if (panelState.pendingCommandPoll) {
clearInterval(panelState.pendingCommandPoll);
panelState.pendingCommandPoll = null;
}
panelState.pendingCommandId = null;
};
const persistentCancelContainerId = panelId + '-persistent-cancel';
const showPersistentCancelButton = function (commandId) {
hidePersistentCancelButton();
const container = document.createElement('div');
container.id = persistentCancelContainerId;
container.style.marginTop = '0.35rem';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'button is-danger is-light is-small compose-persistent-cancel-btn';
btn.textContent = 'Cancel Queued Send';
btn.addEventListener('click', function () {
const payload = new URLSearchParams();
payload.set('service', thread.dataset.service || '');
payload.set('identifier', thread.dataset.identifier || '');
payload.set('command_id', String(commandId || ''));
fetch('{% url "compose_cancel_send" %}', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': '{{ csrf_token }}', 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload.toString(),
}).then(function (resp) {
stopPendingCommandPolling();
hidePersistentCancelButton();
setStatus('Send cancelled.', 'warning');
poll(true);
}).catch(function () {
hidePersistentCancelButton();
});
});
container.appendChild(btn);
if (statusBox) {
statusBox.appendChild(container);
}
};
const hidePersistentCancelButton = function () {
try {
const el = document.getElementById(persistentCancelContainerId);
if (el) el.remove();
} catch (e) {}
};
document.body.addEventListener('composeSendCommandId', function (ev) {
try {
const detail = (ev && ev.detail) || {};
const cmd = (detail && detail.command_id) || (detail && detail.composeSendCommandId && detail.composeSendCommandId.command_id) || null;
if (cmd) {
startPendingCommandPolling(String(cmd));
}
} catch (e) {}
});
panelState.sendResultHandler = function (event) {
const detail = (event && event.detail) || {};
const sourcePanelId = String(detail.panel_id || "");