Implement executing tasks

This commit is contained in:
2026-03-03 16:41:28 +00:00
parent d6bd56dace
commit 9c14e51b43
42 changed files with 3410 additions and 121 deletions

View File

@@ -142,6 +142,18 @@
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>AI Workspace</span>
</a>
{% if ai_workspace_graphs_url and ai_workspace_info_url %}
<span class="compose-insights-capsule">
<a class="compose-insights-link" href="{{ ai_workspace_graphs_url }}">
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
<span>Graphs</span>
</a>
<a class="compose-insights-link" href="{{ ai_workspace_info_url }}">
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
<span>Info</span>
</a>
</span>
{% endif %}
{% if ai_workspace_widget_url %}
<button
type="button"
@@ -354,6 +366,20 @@
aria-label="Export conversation history"></textarea>
</div>
<div
id="{{ panel_id }}-availability"
class="compose-availability-lane{% if not availability_enabled %} is-hidden{% endif %}"
data-slices='{{ availability_slices_json|default:"[]"|escapejs }}'
aria-label="Contact availability timeline">
{% for row in availability_slices %}
<span
class="compose-availability-chip is-{{ row.state }}"
title="{{ row.state|title }} via {{ row.service|upper }} ({{ row.confidence_end|floatformat:2 }})">
{{ row.state|title }} · {{ row.service|upper }}
</span>
{% endfor %}
</div>
<div
id="{{ panel_id }}-thread"
class="compose-thread"
@@ -553,6 +579,45 @@
padding: 0.65rem;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.7), rgba(255, 255, 255, 0.98));
}
#{{ panel_id }} .compose-availability-lane {
margin-top: 0.42rem;
display: flex;
flex-wrap: wrap;
gap: 0.24rem;
}
#{{ panel_id }} .compose-availability-lane.is-hidden {
display: none;
}
#{{ panel_id }} .compose-availability-chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.06rem 0.45rem;
font-size: 0.62rem;
border: 1px solid rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.95);
color: #35465a;
line-height: 1.2;
}
#{{ panel_id }} .compose-availability-chip.is-available {
border-color: rgba(28, 144, 77, 0.4);
background: rgba(228, 249, 237, 0.98);
color: #1a6f3d;
}
#{{ panel_id }} .compose-availability-chip.is-fading {
border-color: rgba(187, 119, 18, 0.4);
background: rgba(255, 247, 230, 0.98);
color: #8a5b13;
}
#{{ panel_id }} .compose-availability-chip.is-unavailable {
border-color: rgba(194, 37, 37, 0.35);
background: rgba(255, 236, 236, 0.98);
color: #8f1e1e;
}
#{{ panel_id }} .compose-body,
#{{ panel_id }} .compose-reaction-chip {
font-family: inherit, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
}
#{{ panel_id }} .compose-row {
display: flex;
flex-direction: column;
@@ -572,6 +637,32 @@
#{{ panel_id }} .compose-row.is-out {
align-items: flex-end;
}
#{{ panel_id }} .compose-insights-capsule {
display: inline-flex;
border: 1px solid rgba(38, 77, 127, 0.28);
border-radius: 999px;
overflow: hidden;
background: linear-gradient(180deg, rgba(240, 246, 255, 0.96), rgba(233, 242, 255, 0.9));
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.85) inset;
}
#{{ panel_id }} .compose-insights-link {
display: inline-flex;
align-items: center;
gap: 0.24rem;
padding: 0.32rem 0.62rem;
font-size: 0.7rem;
font-weight: 600;
color: #1f4f82;
text-decoration: none;
line-height: 1;
}
#{{ panel_id }} .compose-insights-link + .compose-insights-link {
border-left: 1px solid rgba(38, 77, 127, 0.22);
}
#{{ panel_id }} .compose-insights-link:hover {
background: rgba(219, 234, 255, 0.92);
color: #153a63;
}
#{{ panel_id }} .compose-row.is-group-middle,
#{{ panel_id }} .compose-row.is-group-first {
margin-bottom: 0.16rem;
@@ -1759,6 +1850,7 @@
const exportCopy = document.getElementById(panelId + "-export-copy");
const exportClear = document.getElementById(panelId + "-export-clear");
const exportBuffer = document.getElementById(panelId + "-export-buffer");
const availabilityLane = document.getElementById(panelId + "-availability");
const csrfToken = "{{ csrf_token }}";
if (lightbox && lightbox.parentElement !== document.body) {
document.body.appendChild(lightbox);
@@ -3025,6 +3117,40 @@
updateExportBuffer();
};
const renderAvailabilitySlices = function (slices) {
if (!availabilityLane) {
return;
}
const rows = Array.isArray(slices) ? slices : [];
availabilityLane.innerHTML = "";
if (!rows.length) {
availabilityLane.classList.add("is-hidden");
return;
}
rows.forEach(function (item) {
const chip = document.createElement("span");
const state = String((item && item.state) || "unknown").toLowerCase();
const service = String((item && item.service) || "").toUpperCase();
const confidence = Number((item && item.confidence_end) || 0);
const payload = (item && typeof item.payload === "object" && item.payload) ? item.payload : {};
const inferredFrom = String(payload.inferred_from || payload.extended_by || "").trim();
const lastSeenTs = Number(payload.last_seen_ts || 0);
chip.className = "compose-availability-chip is-" + state;
chip.textContent = (state.charAt(0).toUpperCase() + state.slice(1)) + (service ? (" · " + service) : "");
const meta = [];
meta.push("confidence " + confidence.toFixed(2));
if (inferredFrom) {
meta.push("source " + inferredFrom);
}
if (lastSeenTs > 0) {
meta.push("last seen " + new Date(lastSeenTs).toLocaleString());
}
chip.title = chip.textContent + " (" + meta.join(", ") + ")";
availabilityLane.appendChild(chip);
});
availabilityLane.classList.remove("is-hidden");
};
const applyTyping = function (typingPayload) {
if (!typingNode || !typingPayload || typeof typingPayload !== "object") {
return;
@@ -3059,6 +3185,9 @@
if (payload.typing) {
applyTyping(payload.typing);
}
if (payload.availability_slices) {
renderAvailabilitySlices(payload.availability_slices);
}
if (payload.last_ts !== undefined && payload.last_ts !== null) {
lastTs = Math.max(lastTs, toInt(payload.last_ts));
thread.dataset.lastTs = String(lastTs);
@@ -3096,6 +3225,9 @@
if (payload.typing) {
applyTyping(payload.typing);
}
if (payload.availability_slices) {
renderAvailabilitySlices(payload.availability_slices);
}
if (payload.last_ts !== undefined && payload.last_ts !== null) {
lastTs = Math.max(lastTs, toInt(payload.last_ts));
thread.dataset.lastTs = String(lastTs);
@@ -3136,8 +3268,14 @@
}
updateManualSafety();
try {
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
applyTyping(initialTyping);
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
applyTyping(initialTyping);
try {
const initialSlices = JSON.parse(String((availabilityLane && availabilityLane.dataset.slices) || "[]"));
renderAvailabilitySlices(initialSlices);
} catch (err) {
renderAvailabilitySlices([]);
}
} catch (err) {
// Ignore invalid initial typing state payload.
}

View File

@@ -12,6 +12,7 @@
<th>account</th>
<th>name</th>
<th>person</th>
<th>availability</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@@ -36,6 +37,13 @@
{% if item.chat %}{{ item.chat.source_name }}{% else %}{{ item.name }}{% endif %}
</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
{% if item.availability_label %}
<span class="tag is-light">{{ item.availability_label }}</span>
{% else %}
-
{% endif %}
</td>
<td>
<div class="buttons">
{% if not item.is_group %}

View File

@@ -10,6 +10,7 @@
<th>chat</th>
<th>identifier</th>
<th>person</th>
<th>availability</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@@ -25,6 +26,13 @@
</a>
</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
{% if item.availability_label %}
<span class="tag is-light">{{ item.availability_label }}</span>
{% else %}
-
{% endif %}
</td>
<td>
<div class="buttons">
{% if type == 'page' %}
@@ -52,7 +60,7 @@
</tr>
{% empty %}
<tr>
<td colspan="4" class="has-text-grey">No WhatsApp chats discovered yet.</td>
<td colspan="5" class="has-text-grey">No WhatsApp chats discovered yet.</td>
</tr>
{% endfor %}
</table>