Pull groups from WhatsApp

This commit is contained in:
2026-02-18 21:22:45 +00:00
parent 521692c458
commit c400c46e7d
12 changed files with 643 additions and 136 deletions

View File

@@ -0,0 +1,84 @@
{% extends "index.html" %}
{% block content %}
<section class="section">
<div class="container" style="max-width: 44rem;">
<div class="level" style="margin-bottom: 0.75rem;">
<div class="level-left">
<div>
<h1 class="title is-4" style="margin-bottom: 0.2rem;">WhatsApp Chat Link</h1>
<p class="is-size-7 has-text-grey">Link a WhatsApp chat identifier to a person. This link is WhatsApp-only.</p>
</div>
</div>
<div class="level-right">
<a class="button is-light" href="{% url 'whatsapp' %}">
<span class="icon is-small"><i class="fa-solid fa-arrow-left"></i></span>
<span>Back To WhatsApp</span>
</a>
</div>
</div>
{% if notice_message %}
<article class="notification is-{{ notice_level|default:'info' }} is-light">
{{ notice_message }}
</article>
{% endif %}
<article class="box">
<form method="post">
{% csrf_token %}
<div class="field">
<label class="label is-small">Chat Identifier</label>
<div class="control">
<input class="input" type="text" name="identifier" value="{{ identifier }}" required>
</div>
</div>
<div class="field">
<label class="label is-small">Existing Person</label>
<div class="select is-fullwidth">
<select name="person_id">
<option value="">- Select person -</option>
{% for person in people %}
<option value="{{ person.id }}" {% if existing and existing.person_id == person.id %}selected{% endif %}>{{ person.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-small">Or Create Person</label>
<div class="control">
<input class="input" type="text" name="person_name" placeholder="New person name">
</div>
</div>
<div class="field">
<label class="label is-small">Chat JID (optional)</label>
<div class="control">
<input class="input" type="text" name="chat_jid" value="{% if existing %}{{ existing.chat_jid|default:'' }}{% endif %}">
</div>
</div>
<div class="field">
<label class="label is-small">Display Name (optional)</label>
<div class="control">
<input class="input" type="text" name="chat_name" value="{% if existing %}{{ existing.chat_name|default:'' }}{% endif %}">
</div>
</div>
<button class="button is-link" type="submit">
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
<span>Save WhatsApp Chat Link</span>
</button>
</form>
</article>
{% if existing %}
<article class="notification is-light" style="margin-top: 0.8rem;">
Current link: <strong>{{ existing.person.name }}</strong><code>{{ existing.chat_identifier }}</code>
</article>
{% endif %}
</div>
</section>
{% endblock %}

View File

@@ -2545,18 +2545,6 @@
return response.json();
};
const getJson = async function (url) {
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
};
const titleCase = function (value) {
const raw = String(value || "").trim().toLowerCase();
if (!raw) {
@@ -2639,17 +2627,6 @@
}
};
const cardContentNode = function (card) {
return card ? card.querySelector(".compose-ai-content") : null;
};
const setCardMessage = function (card, message) {
const node = cardContentNode(card);
if (node) {
node.textContent = String(message || "");
}
};
const openEngage = function (sourceRef) {
const engageCard = showCard("engage");
if (!engageCard) {
@@ -2669,19 +2646,19 @@
}
setCardLoading(card, true);
try {
const payload = await getJson(
thread.dataset.draftsUrl + "?" + queryParams().toString()
);
const response = await fetch(thread.dataset.draftsUrl + "?" + queryParams().toString(), {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
const payload = await response.json();
setCardLoading(card, false);
if (!payload.ok) {
setCardMessage(card, payload.error || "Failed to load drafts.");
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load drafts.";
return;
}
const drafts = Array.isArray(payload.drafts) ? payload.drafts : [];
const container = cardContentNode(card);
if (!container) {
return;
}
const container = card.querySelector(".compose-ai-content");
container.innerHTML = "";
const engageButton = document.createElement("button");
engageButton.type = "button";
@@ -2720,7 +2697,7 @@
});
} catch (err) {
setCardLoading(card, false);
setCardMessage(card, "Failed to load drafts.");
card.querySelector(".compose-ai-content").textContent = "Failed to load drafts.";
}
};
@@ -2731,18 +2708,21 @@
}
setCardLoading(card, true);
try {
const payload = await getJson(
thread.dataset.summaryUrl + "?" + queryParams().toString()
);
const response = await fetch(thread.dataset.summaryUrl + "?" + queryParams().toString(), {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
const payload = await response.json();
setCardLoading(card, false);
if (!payload.ok) {
setCardMessage(card, payload.error || "Failed to load summary.");
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load summary.";
return;
}
setCardMessage(card, String(payload.summary || ""));
card.querySelector(".compose-ai-content").textContent = String(payload.summary || "");
} catch (err) {
setCardLoading(card, false);
setCardMessage(card, "Failed to load summary.");
card.querySelector(".compose-ai-content").textContent = "Failed to load summary.";
}
};
@@ -2753,14 +2733,17 @@
}
setCardLoading(card, true);
try {
const payload = await getJson(
thread.dataset.quickInsightsUrl + "?" + queryParams().toString()
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 = cardContentNode(card);
if (!container) {
return;
}
const container = card.querySelector(".compose-ai-content");
if (!payload.ok) {
container.textContent = payload.error || "Failed to load quick insights.";
return;
@@ -3013,7 +2996,8 @@
}
} catch (err) {
setCardLoading(card, false);
setCardMessage(card, "Failed to load quick insights.");
card.querySelector(".compose-ai-content").textContent =
"Failed to load quick insights.";
}
};
@@ -3053,12 +3037,18 @@
if (showCustom && customValue) {
params.set("custom_text", customValue);
}
const payload = await getJson(
thread.dataset.engagePreviewUrl + "?" + params.toString()
const response = await fetch(
thread.dataset.engagePreviewUrl + "?" + params.toString(),
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
}
);
const payload = await response.json();
setCardLoading(card, false);
if (!payload.ok) {
setCardMessage(card, payload.error || "Failed to load engage preview.");
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load engage preview.";
panelState.engageToken = "";
return;
}
@@ -3093,11 +3083,11 @@
if (payload.artifact) {
text = text + "\n\nSource: " + String(payload.artifact);
}
setCardMessage(card, text);
card.querySelector(".compose-ai-content").textContent = text;
sendBtn.disabled = !(confirm.checked && panelState.engageToken);
} catch (err) {
setCardLoading(card, false);
setCardMessage(card, "Failed to load engage preview.");
card.querySelector(".compose-ai-content").textContent = "Failed to load engage preview.";
panelState.engageToken = "";
} finally {
if (refreshBtn) {