Rebuild workspace widgets and behavioral graph views

This commit is contained in:
2026-03-13 16:48:24 +00:00
parent f8a6d1d41c
commit 57269770b5
47 changed files with 2951 additions and 1077 deletions

View File

@@ -40,7 +40,18 @@ from core.models import (
WorkspaceConversation,
WorkspaceMetricSnapshot,
)
from core.workspace import DENSITY_POINT_CAPS, downsample_points
from core.widget_ids import compose_widget_dom_id
from core.workspace import (
BEHAVIORAL_GROUPS,
BEHAVIORAL_METRIC_MAP,
BEHAVIORAL_METRIC_SPECS,
DENSITY_POINT_CAPS,
build_behavioral_graph_payload,
build_behavioral_metric_groups,
downsample_points,
get_behavioral_metric_graph,
sanitize_graph_range,
)
SEND_ENABLED_MODES = {"active", "instant"}
OPERATION_LABELS = {
@@ -735,6 +746,22 @@ def _compose_widget_url_for_person(user, person, limit=40):
return f"{reverse('compose_widget')}?{query}"
def _compose_widget_id_for_person(user, person):
preferred_service = _preferred_service_for_person(user, person)
identifier_row = _resolve_person_identifier(
user=user,
person=person,
preferred_service=preferred_service,
)
if identifier_row is None:
return ""
return compose_widget_dom_id(
identifier_row.service,
identifier_row.identifier,
person.id,
)
def _participant_feedback_display(conversation, person):
payload = conversation.participant_feedback or {}
if not isinstance(payload, dict):
@@ -3514,18 +3541,89 @@ def _workspace_nav_urls(person):
"ai_workspace_insight_graphs",
kwargs={"type": "page", "person_id": person.id},
),
"graphs_widget_url": reverse(
"ai_workspace_insight_graphs",
kwargs={"type": "widget", "person_id": person.id},
),
"information_url": reverse(
"ai_workspace_information",
kwargs={"type": "page", "person_id": person.id},
),
"information_widget_url": reverse(
"ai_workspace_information",
kwargs={"type": "widget", "person_id": person.id},
),
"help_url": reverse(
"ai_workspace_insight_help",
kwargs={"type": "page", "person_id": person.id},
),
"help_widget_url": reverse(
"ai_workspace_insight_help",
kwargs={"type": "widget", "person_id": person.id},
),
"workspace_url": f"{reverse('ai_workspace')}?person={person.id}",
}
def _behavioral_metric_launch_groups(person):
return build_behavioral_metric_groups(
lambda spec: {
"slug": spec["slug"],
"title": spec["menu_title"],
"icon": spec["icon"],
"widget_url": reverse(
"ai_workspace_insight_detail",
kwargs={
"type": "widget",
"person_id": person.id,
"metric": spec["slug"],
},
),
"page_url": reverse(
"ai_workspace_insight_detail",
kwargs={
"type": "page",
"person_id": person.id,
"metric": spec["slug"],
},
),
}
)
def _render_ai_workspace_widget(
request,
*,
title,
unique,
window_content,
widget_icon,
widget_options,
**context,
):
return render(
request,
"mixins/wm/widget.html",
{
"title": title,
"unique": unique,
"window_content": window_content,
"widget_icon": widget_icon,
"widget_options": widget_options,
**context,
},
)
def _behavioral_range_urls(request):
return {
"30d": f"{request.path}?range=30d",
"90d": f"{request.path}?range=90d",
"365d": f"{request.path}?range=365d",
"all": f"{request.path}?range=all",
}
class AIWorkspace(LoginRequiredMixin, View):
template_name = "pages/ai-workspace.html"
@@ -3641,12 +3739,23 @@ class AIWorkspacePersonWidget(LoginRequiredMixin, View):
person,
limit=limit,
),
"compose_widget_id": _compose_widget_id_for_person(request.user, person),
"compose_widget_base_url": reverse("compose_widget"),
"history_widget_url": (
reverse("compose_workspace_history_widget")
+ "?"
+ urlencode({"person": str(person.id), "limit": limit})
),
"behavioral_graphs_widget_url": reverse(
"ai_workspace_insight_graphs",
kwargs={"type": "widget", "person_id": person.id},
),
"behavioral_graphs_page_url": reverse(
"ai_workspace_insight_graphs",
kwargs={"type": "page", "person_id": person.id},
),
"behavioral_graph_groups": _behavioral_metric_launch_groups(person),
"behavioral_show_widget_actions": True,
"manual_icon_class": "fa-solid fa-paper-plane",
"send_target_bundle": _send_target_options_for_person(request.user, person),
}
@@ -3685,37 +3794,43 @@ class AIWorkspaceInsightDetail(LoginRequiredMixin, View):
def get(self, request, type, person_id, metric):
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
spec = INSIGHT_METRICS.get(metric)
if spec is None:
if str(metric or "").strip() not in BEHAVIORAL_METRIC_MAP:
return HttpResponseBadRequest("Unknown insight metric")
person = get_object_or_404(Person, pk=person_id, user=request.user)
conversation = _conversation_for_person(request.user, person)
latest_snapshot = conversation.metric_snapshots.first()
value = _format_metric_value(conversation, metric, latest_snapshot)
group = INSIGHT_GROUPS[spec["group"]]
graph_applicable = _metric_supports_history(metric, spec)
graph_density = _sanitize_graph_density(request.GET.get("density"))
points = []
if graph_applicable:
points = _history_points(
conversation, spec["history_field"], density=graph_density
)
range_key = sanitize_graph_range(request.GET.get("range"))
payload = get_behavioral_metric_graph(
user=request.user,
person=person,
metric_slug=metric,
range_key=range_key,
density=graph_density,
)
context = {
"person": person,
"workspace_conversation": conversation,
"metric_slug": metric,
"metric": spec,
"metric_value": value,
"metric_psychology_hint": _metric_psychological_read(metric, conversation),
"metric_group": group,
"graph_points": points,
"graph_applicable": graph_applicable,
"behavioral_groups": BEHAVIORAL_GROUPS,
"metric": payload["metric"],
"summary_cards": payload["summary_cards"],
"coverage": payload["coverage"],
"range_key": range_key,
"graph_density": graph_density,
"graph_density_caps": DENSITY_POINT_CAPS,
"behavioral_graph_groups": _behavioral_metric_launch_groups(person),
"behavioral_show_widget_actions": type == "widget",
"behavioral_range_urls": _behavioral_range_urls(request),
**_workspace_nav_urls(person),
}
if type == "widget":
return _render_ai_workspace_widget(
request,
title=f"{person.name}: {payload['metric']['title']}",
unique=f"ai-behavior-detail-{person.id}-{metric}",
window_content="partials/ai-workspace-behavioral-graph-detail.html",
widget_icon=payload["metric"]["icon"],
widget_options='gs-w="6" gs-h="10" gs-x="6" gs-y="0" gs-min-w="4"',
**context,
)
return render(request, "pages/ai-workspace-insight-detail.html", context)
@@ -3727,17 +3842,37 @@ class AIWorkspaceInsightGraphs(LoginRequiredMixin, View):
return HttpResponseBadRequest("Invalid type specified")
person = get_object_or_404(Person, pk=person_id, user=request.user)
conversation = _conversation_for_person(request.user, person)
graph_density = _sanitize_graph_density(request.GET.get("density"))
graph_cards = _all_graph_payload(conversation, density=graph_density)
range_key = sanitize_graph_range(request.GET.get("range"))
payload = build_behavioral_graph_payload(
user=request.user,
person=person,
range_key=range_key,
density=graph_density,
)
context = {
"person": person,
"workspace_conversation": conversation,
"graph_cards": graph_cards,
"behavioral_groups": BEHAVIORAL_GROUPS,
"graph_cards": payload["graphs"],
"summary_cards": payload["summary_cards"],
"coverage": payload["coverage"],
"range_key": range_key,
"graph_density": graph_density,
"graph_density_caps": DENSITY_POINT_CAPS,
"behavioral_graph_groups": _behavioral_metric_launch_groups(person),
"behavioral_show_widget_actions": type == "widget",
"behavioral_range_urls": _behavioral_range_urls(request),
**_workspace_nav_urls(person),
}
if type == "widget":
return _render_ai_workspace_widget(
request,
title=f"{person.name}: Behavioral Graphs",
unique=f"ai-behavior-graphs-{person.id}",
window_content="partials/ai-workspace-behavioral-graphs.html",
widget_icon="fa-solid fa-chart-line",
widget_options='gs-w="8" gs-h="11" gs-x="4" gs-y="0" gs-min-w="4"',
**context,
)
return render(request, "pages/ai-workspace-insight-graphs.html", context)
@@ -3749,39 +3884,38 @@ class AIWorkspaceInformation(LoginRequiredMixin, View):
return HttpResponseBadRequest("Invalid type specified")
person = get_object_or_404(Person, pk=person_id, user=request.user)
conversation = _conversation_for_person(request.user, person)
latest_snapshot = conversation.metric_snapshots.first()
directionality = _commitment_directionality_payload(conversation)
graph_density = _sanitize_graph_density(request.GET.get("density"))
commitment_graph_cards = [
card
for card in _all_graph_payload(conversation, density=graph_density)
if card["group"] == "commitment"
]
graph_refs = []
for ref in directionality.get("graph_refs", []):
slug = ref.get("slug")
if not slug:
continue
graph_refs.append(
{
**ref,
"slug": slug,
"value": _format_metric_value(conversation, slug, latest_snapshot),
}
)
directionality["graph_refs"] = graph_refs
range_key = sanitize_graph_range(request.GET.get("range"))
payload = build_behavioral_graph_payload(
user=request.user,
person=person,
range_key=range_key,
density=graph_density,
)
context = {
"person": person,
"workspace_conversation": conversation,
"directionality": directionality,
"overview_rows": _information_overview_rows(conversation),
"commitment_graph_cards": commitment_graph_cards,
"behavioral_groups": BEHAVIORAL_GROUPS,
"summary_cards": payload["summary_cards"],
"graph_cards": payload["graphs"],
"coverage": payload["coverage"],
"range_key": range_key,
"graph_density": graph_density,
"behavioral_graph_groups": _behavioral_metric_launch_groups(person),
"behavioral_show_widget_actions": type == "widget",
"behavioral_range_urls": _behavioral_range_urls(request),
**_workspace_nav_urls(person),
}
if type == "widget":
return _render_ai_workspace_widget(
request,
title=f"{person.name}: MS/PS Information",
unique=f"ai-behavior-info-{person.id}",
window_content="partials/ai-workspace-behavioral-information.html",
widget_icon="fa-solid fa-circle-info",
widget_options='gs-w="6" gs-h="10" gs-x="6" gs-y="0" gs-min-w="4"',
**context,
)
return render(request, "pages/ai-workspace-information.html", context)
@@ -3793,33 +3927,37 @@ class AIWorkspaceInsightHelp(LoginRequiredMixin, View):
return HttpResponseBadRequest("Invalid type specified")
person = get_object_or_404(Person, pk=person_id, user=request.user)
conversation = _conversation_for_person(request.user, person)
latest_snapshot = conversation.metric_snapshots.first()
metrics = []
for slug, spec in INSIGHT_METRICS.items():
metrics.append(
{
"slug": slug,
"title": spec["title"],
"group": spec["group"],
"group_title": INSIGHT_GROUPS[spec["group"]]["title"],
"calculation": spec["calculation"],
"psychology": spec["psychology"],
"value": _format_metric_value(
conversation,
slug,
latest_snapshot,
),
}
)
graph_density = _sanitize_graph_density(request.GET.get("density"))
range_key = sanitize_graph_range(request.GET.get("range"))
payload = build_behavioral_graph_payload(
user=request.user,
person=person,
range_key=range_key,
density=graph_density,
)
context = {
"person": person,
"workspace_conversation": conversation,
"groups": INSIGHT_GROUPS,
"metrics": metrics,
"groups": BEHAVIORAL_GROUPS,
"metrics": payload["graphs"],
"summary_cards": payload["summary_cards"],
"coverage": payload["coverage"],
"range_key": range_key,
"behavioral_graph_groups": _behavioral_metric_launch_groups(person),
"behavioral_show_widget_actions": type == "widget",
"behavioral_range_urls": _behavioral_range_urls(request),
**_workspace_nav_urls(person),
}
if type == "widget":
return _render_ai_workspace_widget(
request,
title=f"{person.name}: MS/PS Help",
unique=f"ai-behavior-help-{person.id}",
window_content="partials/ai-workspace-behavioral-help.html",
widget_icon="fa-solid fa-circle-question",
widget_options='gs-w="6" gs-h="10" gs-x="6" gs-y="0" gs-min-w="4"',
**context,
)
return render(request, "pages/ai-workspace-insight-help.html", context)