Renovate mitigation panels and messages
This commit is contained in:
@@ -13,7 +13,7 @@ from django.core import signing
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils import timezone as dj_timezone
|
||||
from django.views import View
|
||||
|
||||
@@ -426,10 +426,14 @@ def _build_gap_fragment(is_outgoing_reply, lag_ms, snapshot):
|
||||
)
|
||||
if score_value is None:
|
||||
score_value = _score_from_lag_for_thread(lag_ms)
|
||||
score_value = max(0.0, min(100.0, float(score_value)))
|
||||
return {
|
||||
"slug": metric_slug,
|
||||
"title": "Unseen Gap",
|
||||
"focus": "Your reply delay" if is_outgoing_reply else "Counterpart reply delay",
|
||||
"lag": _format_gap_duration(lag_ms),
|
||||
"lag_ms": int(lag_ms or 0),
|
||||
"score_value": round(score_value, 2),
|
||||
"score": _format_metric_fragment_value(score_value, 2),
|
||||
"calculation": copy["calculation"],
|
||||
"psychology": copy["psychology"],
|
||||
@@ -483,6 +487,90 @@ def _serialize_messages_with_artifacts(
|
||||
return serialized
|
||||
|
||||
|
||||
def _insight_detail_url(person_id, metric_slug):
|
||||
if not person_id or not metric_slug:
|
||||
return ""
|
||||
try:
|
||||
return reverse(
|
||||
"ai_workspace_insight_detail",
|
||||
kwargs={
|
||||
"type": "page",
|
||||
"person_id": person_id,
|
||||
"metric": str(metric_slug),
|
||||
},
|
||||
)
|
||||
except NoReverseMatch:
|
||||
return ""
|
||||
|
||||
|
||||
def _glance_items_from_state(gap_fragment=None, metric_fragments=None, person_id=None):
|
||||
items = []
|
||||
if gap_fragment:
|
||||
tooltip_parts = [
|
||||
f"{gap_fragment.get('focus') or 'Response delay'}",
|
||||
f"Delay {gap_fragment.get('lag') or '-'}",
|
||||
f"Score {gap_fragment.get('score') or '-'}",
|
||||
]
|
||||
if gap_fragment.get("calculation"):
|
||||
tooltip_parts.append(
|
||||
f"How it is calculated: {gap_fragment.get('calculation')}"
|
||||
)
|
||||
if gap_fragment.get("psychology"):
|
||||
tooltip_parts.append(
|
||||
f"Psychological interpretation: {gap_fragment.get('psychology')}"
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"label": "Response Delay",
|
||||
"value": f"{gap_fragment.get('lag') or '-'} · {gap_fragment.get('score') or '-'}",
|
||||
"tooltip": " | ".join(tooltip_parts),
|
||||
"url": _insight_detail_url(
|
||||
person_id,
|
||||
gap_fragment.get("slug") or "inbound_response_score",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
metric_fragments = list(metric_fragments or [])
|
||||
for metric in metric_fragments[:2]:
|
||||
tooltip_parts = []
|
||||
if metric.get("calculation"):
|
||||
tooltip_parts.append(f"How it is calculated: {metric.get('calculation')}")
|
||||
if metric.get("psychology"):
|
||||
tooltip_parts.append(
|
||||
f"Psychological interpretation: {metric.get('psychology')}"
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"label": str(metric.get("title") or "Metric"),
|
||||
"value": str(metric.get("value") or "-"),
|
||||
"tooltip": " | ".join(tooltip_parts),
|
||||
"url": _insight_detail_url(person_id, metric.get("slug")),
|
||||
}
|
||||
)
|
||||
return items[:3]
|
||||
|
||||
|
||||
def _build_glance_items(serialized_messages, person_id=None):
|
||||
rows = list(serialized_messages or [])
|
||||
latest_metrics = []
|
||||
latest_gap = None
|
||||
for row in reversed(rows):
|
||||
row_metrics = list(row.get("metric_fragments") or [])
|
||||
if row_metrics and not latest_metrics:
|
||||
latest_metrics = row_metrics
|
||||
row_gaps = list(row.get("gap_fragments") or [])
|
||||
if row_gaps and latest_gap is None:
|
||||
latest_gap = row_gaps[0]
|
||||
if latest_metrics and latest_gap:
|
||||
break
|
||||
return _glance_items_from_state(
|
||||
latest_gap,
|
||||
latest_metrics,
|
||||
person_id=person_id,
|
||||
)
|
||||
|
||||
|
||||
def _owner_name(user) -> str:
|
||||
return (
|
||||
user.first_name
|
||||
@@ -1040,6 +1128,7 @@ def _compose_urls(service, identifier, person_id):
|
||||
return {
|
||||
"page_url": f"{reverse('compose_page')}?{payload}",
|
||||
"widget_url": f"{reverse('compose_widget')}?{payload}",
|
||||
"workspace_url": f"{reverse('compose_workspace')}?{payload}",
|
||||
}
|
||||
|
||||
|
||||
@@ -1076,6 +1165,15 @@ def _panel_context(
|
||||
counterpart_identifiers = _counterpart_identifiers_for_person(
|
||||
request.user, base["person"]
|
||||
)
|
||||
serialized_messages = _serialize_messages_with_artifacts(
|
||||
session_bundle["messages"],
|
||||
counterpart_identifiers=counterpart_identifiers,
|
||||
conversation=conversation,
|
||||
)
|
||||
glance_items = _build_glance_items(
|
||||
serialized_messages,
|
||||
person_id=(base["person"].id if base["person"] else None),
|
||||
)
|
||||
last_ts = 0
|
||||
if session_bundle["messages"]:
|
||||
last_ts = int(session_bundle["messages"][-1].ts or 0)
|
||||
@@ -1106,11 +1204,9 @@ def _panel_context(
|
||||
"person_identifier": base["person_identifier"],
|
||||
"session": session_bundle["session"],
|
||||
"messages": session_bundle["messages"],
|
||||
"serialized_messages": _serialize_messages_with_artifacts(
|
||||
session_bundle["messages"],
|
||||
counterpart_identifiers=counterpart_identifiers,
|
||||
conversation=conversation,
|
||||
),
|
||||
"serialized_messages": serialized_messages,
|
||||
"glance_items": glance_items,
|
||||
"glance_items_json": json.dumps(glance_items),
|
||||
"last_ts": last_ts,
|
||||
"limit": limit,
|
||||
"notice_message": notice,
|
||||
@@ -1118,6 +1214,9 @@ def _panel_context(
|
||||
"render_mode": render_mode,
|
||||
"compose_page_url": urls["page_url"],
|
||||
"compose_widget_url": urls["widget_url"],
|
||||
"compose_workspace_url": (
|
||||
f"{urls['workspace_url']}&{urlencode({'limit': limit})}"
|
||||
),
|
||||
"compose_drafts_url": reverse("compose_drafts"),
|
||||
"compose_summary_url": reverse("compose_summary"),
|
||||
"compose_engage_preview_url": reverse("compose_engage_preview"),
|
||||
@@ -1137,11 +1236,15 @@ def _panel_context(
|
||||
|
||||
class ComposeContactsDropdown(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
rows = list(
|
||||
all_value = str(request.GET.get("all") or "").strip().lower()
|
||||
fetch_all = all_value in {"1", "true", "yes", "y", "all"}
|
||||
preview_limit = 5
|
||||
queryset = (
|
||||
PersonIdentifier.objects.filter(user=request.user)
|
||||
.select_related("person")
|
||||
.order_by("person__name", "service", "identifier")
|
||||
)
|
||||
rows = list(queryset) if fetch_all else list(queryset[:preview_limit])
|
||||
items = []
|
||||
for row in rows:
|
||||
urls = _compose_urls(row.service, row.identifier, row.person_id)
|
||||
@@ -1159,10 +1262,83 @@ class ComposeContactsDropdown(LoginRequiredMixin, View):
|
||||
{
|
||||
"items": items,
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
"is_preview": not fetch_all,
|
||||
"fetch_contacts_url": f"{reverse('compose_contacts_dropdown')}?all=1",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ComposeWorkspace(LoginRequiredMixin, View):
|
||||
template_name = "pages/compose-workspace.html"
|
||||
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
identifier = str(request.GET.get("identifier") or "").strip()
|
||||
person = None
|
||||
person_id = request.GET.get("person")
|
||||
if person_id:
|
||||
person = Person.objects.filter(id=person_id, user=request.user).first()
|
||||
limit = _safe_limit(request.GET.get("limit") or 40)
|
||||
|
||||
initial_widget_url = ""
|
||||
if identifier or person is not None:
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
if base["identifier"]:
|
||||
urls = _compose_urls(
|
||||
base["service"],
|
||||
base["identifier"],
|
||||
base["person"].id if base["person"] else None,
|
||||
)
|
||||
initial_widget_url = (
|
||||
f"{urls['widget_url']}&{urlencode({'limit': limit})}"
|
||||
)
|
||||
|
||||
contacts_widget_url = (
|
||||
f"{reverse('compose_workspace_contacts_widget')}"
|
||||
f"?{urlencode({'limit': limit})}"
|
||||
)
|
||||
context = {
|
||||
"contacts_widget_url": contacts_widget_url,
|
||||
"initial_widget_url": initial_widget_url,
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class ComposeWorkspaceContactsWidget(LoginRequiredMixin, View):
|
||||
def _contact_rows(self, user):
|
||||
rows = []
|
||||
queryset = (
|
||||
PersonIdentifier.objects.filter(user=user)
|
||||
.select_related("person")
|
||||
.order_by("person__name", "service", "identifier")
|
||||
)
|
||||
for row in queryset:
|
||||
urls = _compose_urls(row.service, row.identifier, row.person_id)
|
||||
rows.append(
|
||||
{
|
||||
"person_name": row.person.name,
|
||||
"service": row.service,
|
||||
"identifier": row.identifier,
|
||||
"compose_widget_url": urls["widget_url"],
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
def get(self, request):
|
||||
limit = _safe_limit(request.GET.get("limit") or 40)
|
||||
context = {
|
||||
"title": "Manual Workspace",
|
||||
"unique": "compose-workspace-contacts",
|
||||
"window_content": "partials/compose-workspace-contacts-widget.html",
|
||||
"widget_options": 'gs-w="4" gs-h="14" gs-x="0" gs-y="0" gs-min-w="3"',
|
||||
"contact_rows": self._contact_rows(request.user),
|
||||
"limit": limit,
|
||||
"limit_options": [20, 40, 60, 100, 200],
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
}
|
||||
return render(request, "mixins/wm/widget.html", context)
|
||||
|
||||
|
||||
class ComposePage(LoginRequiredMixin, View):
|
||||
template_name = "pages/compose.html"
|
||||
|
||||
@@ -1704,7 +1880,7 @@ class ComposeEngageSend(LoginRequiredMixin, View):
|
||||
|
||||
class ComposeSend(LoginRequiredMixin, View):
|
||||
@staticmethod
|
||||
def _response(request, *, ok, message="", level="info"):
|
||||
def _response(request, *, ok, message="", level="info", panel_id=""):
|
||||
response = render(
|
||||
request,
|
||||
"partials/compose-send-status.html",
|
||||
@@ -1718,10 +1894,11 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
"ok": bool(ok),
|
||||
"message": str(message or ""),
|
||||
"level": str(level or "info"),
|
||||
"panel_id": str(panel_id or ""),
|
||||
}
|
||||
}
|
||||
if ok:
|
||||
trigger_payload["composeMessageSent"] = True
|
||||
trigger_payload["composeMessageSent"] = {"panel_id": str(panel_id or "")}
|
||||
response["HX-Trigger"] = json.dumps(trigger_payload)
|
||||
return response
|
||||
|
||||
@@ -1735,6 +1912,7 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
render_mode = str(request.POST.get("render_mode") or "page").strip().lower()
|
||||
if render_mode not in {"page", "widget"}:
|
||||
render_mode = "page"
|
||||
panel_id = str(request.POST.get("panel_id") or "").strip()
|
||||
|
||||
if not identifier and person is None:
|
||||
return HttpResponseBadRequest("Missing contact identifier.")
|
||||
@@ -1747,6 +1925,7 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
ok=False,
|
||||
message="Enable send confirmation before sending.",
|
||||
level="warning",
|
||||
panel_id=panel_id,
|
||||
)
|
||||
|
||||
text = str(request.POST.get("text") or "").strip()
|
||||
@@ -1756,6 +1935,7 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
ok=False,
|
||||
message="Message is empty.",
|
||||
level="danger",
|
||||
panel_id=panel_id,
|
||||
)
|
||||
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
@@ -1771,6 +1951,7 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
ok=False,
|
||||
message="Send failed. Check service account state.",
|
||||
level="danger",
|
||||
panel_id=panel_id,
|
||||
)
|
||||
|
||||
if base["person_identifier"] is not None:
|
||||
@@ -1788,4 +1969,10 @@ class ComposeSend(LoginRequiredMixin, View):
|
||||
custom_author="USER",
|
||||
)
|
||||
|
||||
return self._response(request, ok=True, message="", level="success")
|
||||
return self._response(
|
||||
request,
|
||||
ok=True,
|
||||
message="",
|
||||
level="success",
|
||||
panel_id=panel_id,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user