Tightly integrate WhatsApp selectors into existing UIs

This commit is contained in:
2026-02-16 10:51:57 +00:00
parent a38339c809
commit 15af8af6b2
19 changed files with 2846 additions and 156 deletions

View File

@@ -603,6 +603,81 @@ def _resolve_person_identifier(user, person, preferred_service=None):
return PersonIdentifier.objects.filter(user=user, person=person).first()
def _send_target_options_for_person(user, person):
rows = list(
PersonIdentifier.objects.filter(user=user, person=person)
.exclude(identifier="")
.order_by("service", "identifier", "id")
)
if not rows:
return {"options": [], "selected_id": ""}
preferred_service = _preferred_service_for_person(user, person)
labels = {
"signal": "Signal",
"whatsapp": "WhatsApp",
"instagram": "Instagram",
"xmpp": "XMPP",
}
seen = set()
options = []
for row in rows:
service = str(row.service or "").strip().lower()
identifier = str(row.identifier or "").strip()
if not service or not identifier:
continue
dedupe_key = (service, identifier)
if dedupe_key in seen:
continue
seen.add(dedupe_key)
options.append(
{
"id": str(row.id),
"service": service,
"service_label": labels.get(service, service.title()),
"identifier": identifier,
}
)
if not options:
return {"options": [], "selected_id": ""}
selected_id = options[0]["id"]
if preferred_service:
preferred = next(
(item for item in options if item["service"] == preferred_service),
None,
)
if preferred is not None:
selected_id = preferred["id"]
return {"options": options, "selected_id": selected_id}
def _resolve_person_identifier_target(
user,
person,
target_identifier_id="",
target_service="",
fallback_service=None,
):
target_id = str(target_identifier_id or "").strip()
if target_id:
selected = PersonIdentifier.objects.filter(
user=user,
person=person,
id=target_id,
).first()
if selected is not None:
return selected
preferred = str(target_service or "").strip().lower() or fallback_service
return _resolve_person_identifier(
user=user,
person=person,
preferred_service=preferred,
)
def _preferred_service_for_person(user, person):
"""
Best-effort service hint from the most recent workspace conversation.
@@ -3314,6 +3389,14 @@ def _mitigation_panel_context(
selected_ref = engage_form.get("source_ref") or (
engage_options[0]["value"] if engage_options else ""
)
send_target_bundle = _send_target_options_for_person(plan.user, person)
selected_target_id = str(engage_form.get("target_identifier_id") or "").strip()
if selected_target_id and not any(
item["id"] == selected_target_id for item in send_target_bundle["options"]
):
selected_target_id = ""
if not selected_target_id:
selected_target_id = send_target_bundle["selected_id"]
auto_settings = auto_settings or _get_or_create_auto_settings(
plan.user, plan.conversation
)
@@ -3340,7 +3423,9 @@ def _mitigation_panel_context(
"share_target": engage_form.get("share_target") or "self",
"framing": engage_form.get("framing") or "dont_change",
"context_note": engage_form.get("context_note") or "",
"target_identifier_id": selected_target_id,
},
"send_target_bundle": send_target_bundle,
"send_state": _get_send_state(plan.user, person),
"active_tab": _sanitize_active_tab(active_tab),
"auto_settings": auto_settings,
@@ -3463,12 +3548,15 @@ class AIWorkspacePersonWidget(LoginRequiredMixin, View):
],
"send_state": _get_send_state(request.user, person),
"compose_page_url": _compose_page_url_for_person(request.user, person),
"compose_page_base_url": reverse("compose_page"),
"compose_widget_url": _compose_widget_url_for_person(
request.user,
person,
limit=limit,
),
"compose_widget_base_url": reverse("compose_widget"),
"manual_icon_class": "fa-solid fa-paper-plane",
"send_target_bundle": _send_target_options_for_person(request.user, person),
}
return render(request, "mixins/wm/widget.html", context)
@@ -3799,6 +3887,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
person = get_object_or_404(Person, pk=person_id, user=request.user)
send_state = _get_send_state(request.user, person)
send_target_bundle = _send_target_options_for_person(request.user, person)
conversation = _conversation_for_person(request.user, person)
if operation == "artifacts":
@@ -3859,6 +3948,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
"error": False,
"person": person,
"send_state": send_state,
"send_target_bundle": send_target_bundle,
"ai_result_id": "",
"mitigation_notice_message": mitigation_notice_message,
"mitigation_notice_level": mitigation_notice_level,
@@ -3880,6 +3970,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
"error": True,
"person": person,
"send_state": send_state,
"send_target_bundle": send_target_bundle,
"latest_plan": None,
"latest_plan_rules": [],
"latest_plan_games": [],
@@ -4006,6 +4097,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
"error": False,
"person": person,
"send_state": send_state,
"send_target_bundle": send_target_bundle,
"ai_result_id": str(ai_result.id),
"ai_result_created_at": ai_result.created_at,
"ai_request_status": ai_request.status,
@@ -4035,6 +4127,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
"error": True,
"person": person,
"send_state": send_state,
"send_target_bundle": send_target_bundle,
"latest_plan": None,
"latest_plan_rules": [],
"latest_plan_games": [],
@@ -4074,10 +4167,12 @@ class AIWorkspaceSendDraft(LoginRequiredMixin, View):
},
)
identifier = _resolve_person_identifier(
identifier = _resolve_person_identifier_target(
request.user,
person,
preferred_service=_preferred_service_for_person(request.user, person),
target_identifier_id=request.POST.get("target_identifier_id"),
target_service=request.POST.get("target_service"),
fallback_service=_preferred_service_for_person(request.user, person),
)
if identifier is None:
return render(
@@ -4165,10 +4260,12 @@ class AIWorkspaceQueueDraft(LoginRequiredMixin, View):
},
)
identifier = _resolve_person_identifier(
identifier = _resolve_person_identifier_target(
request.user,
person,
preferred_service=_preferred_service_for_person(request.user, person),
target_identifier_id=request.POST.get("target_identifier_id"),
target_service=request.POST.get("target_service"),
fallback_service=_preferred_service_for_person(request.user, person),
)
if identifier is None:
return render(
@@ -4760,6 +4857,9 @@ class AIWorkspaceEngageShare(LoginRequiredMixin, View):
"share_target": share_target,
"framing": framing,
"context_note": context_note,
"target_identifier_id": str(
request.POST.get("target_identifier_id") or ""
).strip(),
}
active_tab = _sanitize_active_tab(
request.POST.get("active_tab"), default="engage"
@@ -4856,10 +4956,12 @@ class AIWorkspaceEngageShare(LoginRequiredMixin, View):
),
)
identifier = _resolve_person_identifier(
identifier = _resolve_person_identifier_target(
request.user,
person,
preferred_service=plan.conversation.platform_type,
target_identifier_id=request.POST.get("target_identifier_id"),
target_service=request.POST.get("target_service"),
fallback_service=plan.conversation.platform_type,
)
if identifier is None:
return render(
@@ -4955,10 +5057,12 @@ class AIWorkspaceEngageShare(LoginRequiredMixin, View):
return response
if action == "queue":
identifier = _resolve_person_identifier(
identifier = _resolve_person_identifier_target(
request.user,
person,
preferred_service=plan.conversation.platform_type,
target_identifier_id=request.POST.get("target_identifier_id"),
target_service=request.POST.get("target_service"),
fallback_service=plan.conversation.platform_type,
)
if identifier is None:
return render(