Implement more information displays
This commit is contained in:
@@ -27,6 +27,7 @@ from core.models import (
|
||||
PatternMitigationPlan,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
WorkspaceConversation,
|
||||
)
|
||||
from core.realtime.typing_state import get_person_typing_state
|
||||
from core.views.workspace import _build_engage_payload, _parse_draft_options
|
||||
@@ -297,6 +298,279 @@ def _build_summary_prompt(owner_name, person_name, transcript):
|
||||
]
|
||||
|
||||
|
||||
def _to_float(value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _format_number(value, precision=2):
|
||||
number = _to_float(value)
|
||||
if number is None:
|
||||
return "-"
|
||||
rounded = round(number, precision)
|
||||
if float(rounded).is_integer():
|
||||
return str(int(rounded))
|
||||
return f"{rounded:.{precision}f}"
|
||||
|
||||
|
||||
def _percent_change(current, previous):
|
||||
now_val = _to_float(current)
|
||||
prev_val = _to_float(previous)
|
||||
if now_val is None or prev_val is None:
|
||||
return None
|
||||
if abs(prev_val) < 1e-9:
|
||||
return None
|
||||
return ((now_val - prev_val) / abs(prev_val)) * 100.0
|
||||
|
||||
|
||||
def _trend_meta(current, previous, higher_is_better=True):
|
||||
now_val = _to_float(current)
|
||||
prev_val = _to_float(previous)
|
||||
if now_val is None or prev_val is None:
|
||||
return {
|
||||
"direction": "unknown",
|
||||
"icon": "fa-solid fa-minus",
|
||||
"class_name": "has-text-grey",
|
||||
"meaning": "No comparison yet",
|
||||
}
|
||||
delta = now_val - prev_val
|
||||
if abs(delta) < 1e-9:
|
||||
return {
|
||||
"direction": "flat",
|
||||
"icon": "fa-solid fa-minus",
|
||||
"class_name": "has-text-grey",
|
||||
"meaning": "No meaningful change",
|
||||
}
|
||||
is_up = delta > 0
|
||||
improves = is_up if higher_is_better else not is_up
|
||||
return {
|
||||
"direction": "up" if is_up else "down",
|
||||
"icon": "fa-solid fa-arrow-trend-up" if is_up else "fa-solid fa-arrow-trend-down",
|
||||
"class_name": "has-text-success" if improves else "has-text-danger",
|
||||
"meaning": "Improving signal" if improves else "Risk signal",
|
||||
}
|
||||
|
||||
|
||||
def _emotion_meta(metric_kind, value):
|
||||
score = _to_float(value)
|
||||
if score is None:
|
||||
return {
|
||||
"icon": "fa-regular fa-face-meh-blank",
|
||||
"class_name": "has-text-grey",
|
||||
"label": "Unknown",
|
||||
}
|
||||
if metric_kind == "confidence":
|
||||
score = score * 100.0
|
||||
if metric_kind == "count":
|
||||
if score >= 80:
|
||||
return {
|
||||
"icon": "fa-solid fa-chart-column",
|
||||
"class_name": "has-text-success",
|
||||
"label": "Rich Data",
|
||||
}
|
||||
if score >= 30:
|
||||
return {
|
||||
"icon": "fa-solid fa-chart-simple",
|
||||
"class_name": "has-text-warning",
|
||||
"label": "Moderate Data",
|
||||
}
|
||||
return {
|
||||
"icon": "fa-solid fa-chart-line",
|
||||
"class_name": "has-text-danger",
|
||||
"label": "Sparse Data",
|
||||
}
|
||||
if score >= 75:
|
||||
return {
|
||||
"icon": "fa-regular fa-face-smile",
|
||||
"class_name": "has-text-success",
|
||||
"label": "Positive",
|
||||
}
|
||||
if score >= 50:
|
||||
return {
|
||||
"icon": "fa-regular fa-face-meh",
|
||||
"class_name": "has-text-warning",
|
||||
"label": "Mixed",
|
||||
}
|
||||
return {
|
||||
"icon": "fa-regular fa-face-frown",
|
||||
"class_name": "has-text-danger",
|
||||
"label": "Strained",
|
||||
}
|
||||
|
||||
|
||||
def _quick_insights_rows(conversation):
|
||||
latest = conversation.metric_snapshots.first()
|
||||
previous = (
|
||||
conversation.metric_snapshots.order_by("-computed_at")[1:2].first()
|
||||
if conversation.metric_snapshots.count() > 1
|
||||
else None
|
||||
)
|
||||
metric_specs = [
|
||||
{
|
||||
"key": "stability_score",
|
||||
"label": "Stability Score",
|
||||
"field": "stability_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-heart-pulse",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "stability_confidence",
|
||||
"label": "Stability Confidence",
|
||||
"field": "stability_confidence",
|
||||
"source": "conversation",
|
||||
"kind": "confidence",
|
||||
"icon": "fa-solid fa-shield-check",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "sample_messages",
|
||||
"label": "Sample Messages",
|
||||
"field": "stability_sample_messages",
|
||||
"source": "conversation",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-message",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "sample_days",
|
||||
"label": "Sample Days",
|
||||
"field": "stability_sample_days",
|
||||
"source": "conversation",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-calendar-days",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "commitment_inbound",
|
||||
"label": "Commit In",
|
||||
"field": "commitment_inbound_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-inbox",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "commitment_outbound",
|
||||
"label": "Commit Out",
|
||||
"field": "commitment_outbound_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-paper-plane",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "commitment_confidence",
|
||||
"label": "Commit Confidence",
|
||||
"field": "commitment_confidence",
|
||||
"source": "conversation",
|
||||
"kind": "confidence",
|
||||
"icon": "fa-solid fa-badge-check",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "reciprocity",
|
||||
"label": "Reciprocity",
|
||||
"field": "reciprocity_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-right-left",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "continuity",
|
||||
"label": "Continuity",
|
||||
"field": "continuity_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-link",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "response",
|
||||
"label": "Response",
|
||||
"field": "response_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-gauge-high",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "volatility",
|
||||
"label": "Volatility",
|
||||
"field": "volatility_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-wave-square",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "inbound_messages",
|
||||
"label": "Inbound Messages",
|
||||
"field": "inbound_messages",
|
||||
"source": "snapshot",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-arrow-down",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "outbound_messages",
|
||||
"label": "Outbound Messages",
|
||||
"field": "outbound_messages",
|
||||
"source": "snapshot",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-arrow-up",
|
||||
"higher_better": True,
|
||||
},
|
||||
]
|
||||
rows = []
|
||||
for spec in metric_specs:
|
||||
field_name = spec["field"]
|
||||
if spec["source"] == "conversation":
|
||||
current = getattr(conversation, field_name, None)
|
||||
previous_value = getattr(previous, field_name, None) if previous else None
|
||||
else:
|
||||
current = getattr(latest, field_name, None) if latest else None
|
||||
previous_value = getattr(previous, field_name, None) if previous else None
|
||||
trend = _trend_meta(
|
||||
current,
|
||||
previous_value,
|
||||
higher_is_better=spec.get("higher_better", True),
|
||||
)
|
||||
delta_pct = _percent_change(current, previous_value)
|
||||
point_count = conversation.metric_snapshots.exclude(
|
||||
**{f"{field_name}__isnull": True}
|
||||
).count()
|
||||
emotion = _emotion_meta(spec["kind"], current)
|
||||
rows.append(
|
||||
{
|
||||
"key": spec["key"],
|
||||
"label": spec["label"],
|
||||
"icon": spec["icon"],
|
||||
"value": current,
|
||||
"display_value": _format_number(
|
||||
current,
|
||||
3 if spec["kind"] == "confidence" else 2,
|
||||
),
|
||||
"delta_pct": delta_pct,
|
||||
"delta_label": f"{delta_pct:+.2f}%" if delta_pct is not None else "n/a",
|
||||
"point_count": point_count,
|
||||
"trend": trend,
|
||||
"emotion": emotion,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"rows": rows,
|
||||
"snapshot_count": conversation.metric_snapshots.count(),
|
||||
"latest_computed_at": latest.computed_at if latest else None,
|
||||
}
|
||||
|
||||
|
||||
def _build_engage_prompt(owner_name, person_name, transcript):
|
||||
return [
|
||||
{
|
||||
@@ -513,6 +787,7 @@ def _panel_context(
|
||||
"compose_summary_url": reverse("compose_summary"),
|
||||
"compose_engage_preview_url": reverse("compose_engage_preview"),
|
||||
"compose_engage_send_url": reverse("compose_engage_send"),
|
||||
"compose_quick_insights_url": reverse("compose_quick_insights"),
|
||||
"compose_ws_url": ws_url,
|
||||
"ai_workspace_url": (
|
||||
f"{reverse('ai_workspace')}?person={base['person'].id}"
|
||||
@@ -794,6 +1069,94 @@ class ComposeSummary(LoginRequiredMixin, View):
|
||||
return JsonResponse({"ok": True, "cached": False, "summary": summary})
|
||||
|
||||
|
||||
class ComposeQuickInsights(LoginRequiredMixin, View):
|
||||
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 = get_object_or_404(Person, id=person_id, user=request.user)
|
||||
if not identifier and person is None:
|
||||
return JsonResponse({"ok": False, "error": "Missing contact identifier."})
|
||||
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
person = base["person"]
|
||||
if person is None:
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": False,
|
||||
"error": "Quick Insights needs a linked person.",
|
||||
}
|
||||
)
|
||||
|
||||
conversation = (
|
||||
WorkspaceConversation.objects.filter(
|
||||
user=request.user,
|
||||
participants=person,
|
||||
)
|
||||
.order_by("-last_event_ts", "-created_at")
|
||||
.first()
|
||||
)
|
||||
if conversation is None:
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"empty": True,
|
||||
"summary": {
|
||||
"person_name": person.name,
|
||||
"platform": "",
|
||||
"state": "Calibrating",
|
||||
"thread": "",
|
||||
"last_event": "",
|
||||
"last_ai_run": "",
|
||||
"workspace_created": "",
|
||||
"snapshot_count": 0,
|
||||
},
|
||||
"rows": [],
|
||||
"docs": [
|
||||
"Quick Insights needs at least one workspace conversation snapshot.",
|
||||
"Run AI operations in AI Workspace to generate the first data points.",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
payload = _quick_insights_rows(conversation)
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"empty": False,
|
||||
"summary": {
|
||||
"person_name": person.name,
|
||||
"platform": conversation.get_platform_type_display(),
|
||||
"state": conversation.get_stability_state_display(),
|
||||
"thread": conversation.platform_thread_id or "",
|
||||
"last_event": _format_ts_label(conversation.last_event_ts or 0)
|
||||
if conversation.last_event_ts
|
||||
else "",
|
||||
"last_ai_run": (
|
||||
dj_timezone.localtime(conversation.last_ai_run_at).strftime(
|
||||
"%Y-%m-%d %H:%M"
|
||||
)
|
||||
if conversation.last_ai_run_at
|
||||
else ""
|
||||
),
|
||||
"workspace_created": dj_timezone.localtime(
|
||||
conversation.created_at
|
||||
).strftime("%Y-%m-%d %H:%M"),
|
||||
"snapshot_count": payload["snapshot_count"],
|
||||
},
|
||||
"rows": payload["rows"],
|
||||
"docs": [
|
||||
"Each row shows current value, percent change vs previous point, and data-point count.",
|
||||
"Arrow color indicates improving or risk direction for that metric.",
|
||||
"Face indicator maps value range to positive, mixed, or strained climate.",
|
||||
"Use this card for fast triage; open AI Workspace for full graphs and details.",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ComposeEngagePreview(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
|
||||
Reference in New Issue
Block a user