Rebuild workspace widgets and behavioral graph views
This commit is contained in:
@@ -51,18 +51,15 @@ from core.models import (
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
PlatformChatLink,
|
||||
WorkspaceConversation,
|
||||
)
|
||||
from core.presence import get_settings as get_availability_settings
|
||||
from core.presence import latest_state_for_people, spans_for_range
|
||||
from core.realtime.typing_state import get_person_typing_state
|
||||
from core.translation.engine import process_inbound_translation
|
||||
from core.transports.capabilities import supports, unsupported_reason
|
||||
from core.views.workspace import (
|
||||
INSIGHT_METRICS,
|
||||
_build_engage_payload,
|
||||
_parse_draft_options,
|
||||
)
|
||||
from core.views.workspace import _build_engage_payload, _parse_draft_options
|
||||
from core.widget_ids import compose_widget_dom_id, compose_widget_unique
|
||||
from core.workspace import build_behavioral_metric_groups
|
||||
|
||||
COMPOSE_WS_TOKEN_SALT = "compose-ws"
|
||||
COMPOSE_ENGAGE_TOKEN_SALT = "compose-engage"
|
||||
@@ -71,6 +68,7 @@ COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE = 12
|
||||
COMPOSE_WORKSPACE_HISTORY_PAGE_SIZE = 15
|
||||
COMPOSE_THREAD_WINDOW_OPTIONS = [20, 40, 60, 100, 200]
|
||||
COMPOSE_COMMAND_SLUGS = ("bp",)
|
||||
COMPOSE_ASSET_VERSION = "20260313b"
|
||||
URL_PATTERN = re.compile(r"https?://[^\s<>'\"\\]+")
|
||||
SIGNAL_UUID_PATTERN = re.compile(
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
@@ -196,6 +194,14 @@ def _safe_after_ts(raw) -> int:
|
||||
return max(0, value)
|
||||
|
||||
|
||||
def _safe_before_ts(raw) -> int:
|
||||
try:
|
||||
value = int(raw or 0)
|
||||
except (TypeError, ValueError):
|
||||
value = 0
|
||||
return max(0, value)
|
||||
|
||||
|
||||
def _safe_page(raw) -> int:
|
||||
try:
|
||||
value = int(raw or 1)
|
||||
@@ -850,24 +856,7 @@ def _serialize_message(msg: Message) -> dict:
|
||||
},
|
||||
}
|
||||
|
||||
THREAD_METRIC_COPY_OVERRIDES = {
|
||||
"inbound_messages": {
|
||||
"calculation": (
|
||||
"Count of counterpart-to-user messages in the sampled analysis window."
|
||||
),
|
||||
"psychology": (
|
||||
"Lower counts can indicate reduced reach-back or temporary withdrawal."
|
||||
),
|
||||
},
|
||||
"outbound_messages": {
|
||||
"calculation": (
|
||||
"Count of user-to-counterpart messages in the sampled analysis window."
|
||||
),
|
||||
"psychology": (
|
||||
"Large imbalances can reflect chasing or over-functioning dynamics."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def _format_gap_duration(ms_value):
|
||||
value = max(0, int(ms_value or 0))
|
||||
seconds = value // 1000
|
||||
@@ -881,14 +870,8 @@ def _format_gap_duration(ms_value):
|
||||
if rem_minutes == 0:
|
||||
return f"{hours}h"
|
||||
return f"{hours}h {rem_minutes}m"
|
||||
def _metric_copy(slug, fallback_title):
|
||||
spec = INSIGHT_METRICS.get(slug) or {}
|
||||
override = THREAD_METRIC_COPY_OVERRIDES.get(slug) or {}
|
||||
return {
|
||||
"title": spec.get("title") or fallback_title,
|
||||
"calculation": override.get("calculation") or spec.get("calculation") or "",
|
||||
"psychology": override.get("psychology") or spec.get("psychology") or "",
|
||||
}
|
||||
|
||||
|
||||
def _serialize_messages_with_artifacts(messages):
|
||||
rows = list(messages or [])
|
||||
serialized = [_serialize_message(msg) for msg in rows]
|
||||
@@ -964,6 +947,10 @@ def _plain_text(value):
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def _versioned_static_asset(path: str) -> str:
|
||||
return f"{static(path)}?v={COMPOSE_ASSET_VERSION}"
|
||||
|
||||
|
||||
def _engage_body_only(value):
|
||||
lines = [line.strip() for line in str(value or "").splitlines() if line.strip()]
|
||||
if lines and lines[0].startswith("**"):
|
||||
@@ -1051,331 +1038,6 @@ 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, metric_key=None):
|
||||
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":
|
||||
key = str(metric_key or "").strip().lower()
|
||||
if key == "sample_days":
|
||||
if score >= 14:
|
||||
return {
|
||||
"icon": "fa-solid fa-calendar-check",
|
||||
"class_name": "has-text-success",
|
||||
"label": "Broad Coverage",
|
||||
}
|
||||
if score >= 5:
|
||||
return {
|
||||
"icon": "fa-solid fa-calendar-days",
|
||||
"class_name": "has-text-warning",
|
||||
"label": "Adequate Coverage",
|
||||
}
|
||||
return {
|
||||
"icon": "fa-solid fa-calendar-xmark",
|
||||
"class_name": "has-text-danger",
|
||||
"label": "Narrow Coverage",
|
||||
}
|
||||
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",
|
||||
"doc_slug": "stability_score",
|
||||
"field": "stability_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-heart-pulse",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "stability_confidence",
|
||||
"label": "Stability Confidence",
|
||||
"doc_slug": "stability_confidence",
|
||||
"field": "stability_confidence",
|
||||
"source": "conversation",
|
||||
"kind": "confidence",
|
||||
"icon": "fa-solid fa-shield-check",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "sample_messages",
|
||||
"label": "Sample Messages",
|
||||
"doc_slug": "sample_messages",
|
||||
"field": "stability_sample_messages",
|
||||
"source": "conversation",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-message",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "sample_days",
|
||||
"label": "Sample Days",
|
||||
"doc_slug": "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",
|
||||
"doc_slug": "commitment_inbound",
|
||||
"field": "commitment_inbound_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-inbox",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "commitment_outbound",
|
||||
"label": "Commit Out",
|
||||
"doc_slug": "commitment_outbound",
|
||||
"field": "commitment_outbound_score",
|
||||
"source": "conversation",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-paper-plane",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "commitment_confidence",
|
||||
"label": "Commit Confidence",
|
||||
"doc_slug": "commitment_confidence",
|
||||
"field": "commitment_confidence",
|
||||
"source": "conversation",
|
||||
"kind": "confidence",
|
||||
"icon": "fa-solid fa-badge-check",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "reciprocity",
|
||||
"label": "Reciprocity",
|
||||
"doc_slug": "reciprocity_score",
|
||||
"field": "reciprocity_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-right-left",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "continuity",
|
||||
"label": "Continuity",
|
||||
"doc_slug": "continuity_score",
|
||||
"field": "continuity_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-link",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "response",
|
||||
"label": "Response",
|
||||
"doc_slug": "response_score",
|
||||
"field": "response_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-gauge-high",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "volatility",
|
||||
"label": "Volatility",
|
||||
"doc_slug": "volatility_score",
|
||||
"field": "volatility_score",
|
||||
"source": "snapshot",
|
||||
"kind": "score",
|
||||
"icon": "fa-solid fa-wave-square",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "inbound_messages",
|
||||
"label": "Inbound Messages",
|
||||
"doc_slug": "inbound_messages",
|
||||
"field": "inbound_messages",
|
||||
"source": "snapshot",
|
||||
"kind": "count",
|
||||
"icon": "fa-solid fa-arrow-down",
|
||||
"higher_better": True,
|
||||
},
|
||||
{
|
||||
"key": "outbound_messages",
|
||||
"label": "Outbound Messages",
|
||||
"doc_slug": "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"]
|
||||
metric_copy = _metric_copy(spec.get("doc_slug") or spec["key"], spec["label"])
|
||||
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, spec["key"])
|
||||
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,
|
||||
"calculation": metric_copy.get("calculation") or "",
|
||||
"psychology": metric_copy.get("psychology") or "",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"rows": rows,
|
||||
"snapshot_count": conversation.metric_snapshots.count(),
|
||||
"latest_computed_at": latest.computed_at if latest else None,
|
||||
}
|
||||
|
||||
|
||||
def _participant_feedback_state_label(conversation, person):
|
||||
payload = conversation.participant_feedback or {}
|
||||
if not isinstance(payload, dict) or person is None:
|
||||
return ""
|
||||
raw = payload.get(str(person.id)) or {}
|
||||
if not isinstance(raw, dict):
|
||||
return ""
|
||||
state_key = str(raw.get("state") or "").strip().lower()
|
||||
return {
|
||||
"withdrawing": "Withdrawing",
|
||||
"overextending": "Overextending",
|
||||
"balanced": "Balanced",
|
||||
}.get(state_key, "")
|
||||
|
||||
|
||||
def _build_engage_prompt(owner_name, person_name, transcript):
|
||||
return [
|
||||
{
|
||||
@@ -2070,6 +1732,7 @@ def _compose_urls(service, identifier, person_id):
|
||||
return {
|
||||
"page_url": f"{reverse('compose_page')}?{payload}",
|
||||
"widget_url": f"{reverse('compose_widget')}?{payload}",
|
||||
"widget_id": compose_widget_dom_id(service_key, identifier_value, person_id),
|
||||
"workspace_url": f"{reverse('compose_workspace')}?{payload}",
|
||||
}
|
||||
|
||||
@@ -2238,6 +1901,7 @@ def _manual_contact_rows(user):
|
||||
existing["identifier"] = identifier_value
|
||||
existing["compose_url"] = urls["page_url"]
|
||||
existing["compose_widget_url"] = urls["widget_url"]
|
||||
existing["compose_widget_id"] = urls["widget_id"]
|
||||
existing["match_url"] = (
|
||||
f"{reverse('compose_contact_match')}?"
|
||||
f"{urlencode({'service': service_key, 'identifier': identifier_value})}"
|
||||
@@ -2263,6 +1927,7 @@ def _manual_contact_rows(user):
|
||||
"identifier": identifier_value,
|
||||
"compose_url": urls["page_url"],
|
||||
"compose_widget_url": urls["widget_url"],
|
||||
"compose_widget_id": urls["widget_id"],
|
||||
"linked_person": bool(person),
|
||||
"source": source,
|
||||
"match_url": (
|
||||
@@ -2892,6 +2557,34 @@ def _load_messages(user, person_identifier, limit):
|
||||
return {"session": session, "messages": messages}
|
||||
|
||||
|
||||
def _behavioral_metric_launch_groups(person):
|
||||
if person is None:
|
||||
return []
|
||||
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 _panel_context(
|
||||
request,
|
||||
service: str,
|
||||
@@ -2999,6 +2692,20 @@ def _panel_context(
|
||||
+ (f": {error_message[:220]}" if error_message else "")
|
||||
)
|
||||
|
||||
behavioral_graphs_page_url = ""
|
||||
behavioral_graphs_widget_url = ""
|
||||
behavioral_graph_groups = []
|
||||
if base["person"] is not None:
|
||||
behavioral_graphs_page_url = reverse(
|
||||
"ai_workspace_insight_graphs",
|
||||
kwargs={"type": "page", "person_id": base["person"].id},
|
||||
)
|
||||
behavioral_graphs_widget_url = reverse(
|
||||
"ai_workspace_insight_graphs",
|
||||
kwargs={"type": "widget", "person_id": base["person"].id},
|
||||
)
|
||||
behavioral_graph_groups = _behavioral_metric_launch_groups(base["person"])
|
||||
|
||||
return {
|
||||
"service": base["service"],
|
||||
"identifier": base["identifier"],
|
||||
@@ -3018,6 +2725,10 @@ def _panel_context(
|
||||
"platform_options": platform_options,
|
||||
"recent_contacts": recent_contacts,
|
||||
"signal_ingest_warning": signal_ingest_warning,
|
||||
"behavioral_graphs_page_url": behavioral_graphs_page_url,
|
||||
"behavioral_graphs_widget_url": behavioral_graphs_widget_url,
|
||||
"behavioral_graph_groups": behavioral_graph_groups,
|
||||
"behavioral_show_widget_actions": render_mode == "widget",
|
||||
"is_group": base.get("is_group", False),
|
||||
"group_name": base.get("group_name", ""),
|
||||
}
|
||||
@@ -3231,6 +2942,7 @@ class ComposeWorkspaceHistoryWidget(LoginRequiredMixin, View):
|
||||
"display_ts": _format_ts_datetime_label(int(message.ts or 0)),
|
||||
"text_preview": _history_preview_text(message.text or ""),
|
||||
"compose_widget_url": urls["widget_url"],
|
||||
"compose_widget_id": urls["widget_id"],
|
||||
"compose_page_url": urls["page_url"],
|
||||
}
|
||||
)
|
||||
@@ -3586,6 +3298,7 @@ class ComposePage(LoginRequiredMixin, View):
|
||||
person=person,
|
||||
render_mode="page",
|
||||
)
|
||||
context["compose_asset_version"] = COMPOSE_ASSET_VERSION
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
@@ -3607,26 +3320,23 @@ class ComposeWidget(LoginRequiredMixin, View):
|
||||
if panel_context["person"] is not None
|
||||
else panel_context["identifier"]
|
||||
)
|
||||
widget_key = hashlib.sha1(
|
||||
(
|
||||
f"{request.user.pk}:"
|
||||
f"{panel_context['service']}:"
|
||||
f"{panel_context['identifier']}:"
|
||||
f"{getattr(panel_context['person'], 'pk', '')}"
|
||||
).encode("utf-8")
|
||||
).hexdigest()[:12]
|
||||
widget_unique = compose_widget_unique(
|
||||
panel_context["service"],
|
||||
panel_context["identifier"],
|
||||
getattr(panel_context["person"], "pk", None),
|
||||
)
|
||||
context = {
|
||||
"title": f"Manual Chat: {title_name}",
|
||||
"unique": f"compose-widget-{widget_key}",
|
||||
"unique": widget_unique,
|
||||
"window_content": "partials/compose-panel.html",
|
||||
"widget_control_class": "gia-widget-control-no-scroll",
|
||||
"widget_options": 'gs-w="6" gs-h="12" gs-x="0" gs-y="0" gs-min-w="4"',
|
||||
"widget_style_hrefs": [static("css/compose-panel.css")],
|
||||
"widget_style_hrefs": [_versioned_static_asset("css/compose-panel.css")],
|
||||
"widget_script_srcs": [
|
||||
static("js/compose-panel-core.js"),
|
||||
static("js/compose-panel-thread.js"),
|
||||
static("js/compose-panel-send.js"),
|
||||
static("js/compose-panel.js"),
|
||||
_versioned_static_asset("js/compose-panel-core.js"),
|
||||
_versioned_static_asset("js/compose-panel-thread.js"),
|
||||
_versioned_static_asset("js/compose-panel-send.js"),
|
||||
_versioned_static_asset("js/compose-panel.js"),
|
||||
],
|
||||
**panel_context,
|
||||
}
|
||||
@@ -3641,10 +3351,12 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
|
||||
limit = _safe_limit(request.GET.get("limit") or 60)
|
||||
after_ts = _safe_after_ts(request.GET.get("after_ts"))
|
||||
before_ts = _safe_before_ts(request.GET.get("before_ts"))
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
latest_ts = after_ts
|
||||
messages = []
|
||||
seed_previous = None
|
||||
has_older = False
|
||||
session_ids = ComposeHistorySync._session_ids_for_scope(
|
||||
user=request.user,
|
||||
person=base["person"],
|
||||
@@ -3665,7 +3377,9 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
session_id__in=session_ids,
|
||||
)
|
||||
queryset = base_queryset
|
||||
if after_ts > 0:
|
||||
if before_ts > 0:
|
||||
queryset = queryset.filter(ts__lt=before_ts)
|
||||
elif after_ts > 0:
|
||||
seed_previous = (
|
||||
base_queryset.filter(ts__lte=after_ts).order_by("-ts").first()
|
||||
)
|
||||
@@ -3683,6 +3397,9 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
)
|
||||
rows_desc.reverse()
|
||||
messages = rows_desc
|
||||
if messages:
|
||||
oldest_ts = int(messages[0].ts or 0)
|
||||
has_older = base_queryset.filter(ts__lt=oldest_ts).exists()
|
||||
newest = (
|
||||
Message.objects.filter(
|
||||
user=request.user,
|
||||
@@ -3698,6 +3415,7 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
payload = {
|
||||
"messages": serialized_messages,
|
||||
"messages_html": _render_compose_message_rows(serialized_messages),
|
||||
"has_older": has_older,
|
||||
"last_ts": latest_ts,
|
||||
"typing": get_person_typing_state(
|
||||
user_id=request.user.id,
|
||||
@@ -4355,125 +4073,6 @@ class ComposeSummary(LoginRequiredMixin, View):
|
||||
return JsonResponse({"ok": True, "cached": False, "summary": summary})
|
||||
|
||||
|
||||
class ComposeQuickInsights(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
service, identifier, person = _request_scope(request, "GET")
|
||||
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,
|
||||
"platform_docs": _metric_copy("platform", "Platform"),
|
||||
"state_docs": _metric_copy(
|
||||
"stability_state", "Participant State"
|
||||
),
|
||||
"thread_docs": _metric_copy("thread", "Thread"),
|
||||
"snapshot_docs": {
|
||||
"calculation": (
|
||||
"Count of stored workspace metric snapshots for this person."
|
||||
),
|
||||
"psychology": (
|
||||
"More points improve trend reliability; sparse points are "
|
||||
"best treated as directional signals."
|
||||
),
|
||||
},
|
||||
},
|
||||
"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)
|
||||
participant_state = _participant_feedback_state_label(conversation, person)
|
||||
selected_platform_label = _service_label(base["service"])
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"empty": False,
|
||||
"summary": {
|
||||
"person_name": person.name,
|
||||
"platform": selected_platform_label,
|
||||
"platform_scope": "All linked platforms",
|
||||
"state": participant_state
|
||||
or conversation.get_stability_state_display(),
|
||||
"stability_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"],
|
||||
"platform_docs": _metric_copy("platform", "Platform"),
|
||||
"state_docs": _metric_copy("stability_state", "Participant State"),
|
||||
"thread_docs": _metric_copy("thread", "Thread"),
|
||||
"snapshot_docs": {
|
||||
"calculation": (
|
||||
"Count of stored workspace metric snapshots for this person."
|
||||
),
|
||||
"psychology": (
|
||||
"More points improve trend reliability; sparse points are "
|
||||
"best treated as directional signals."
|
||||
),
|
||||
},
|
||||
},
|
||||
"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.",
|
||||
"State uses participant feedback (Withdrawing/Overextending/Balanced) when available.",
|
||||
"Values are computed from all linked platform messages for this person.",
|
||||
"Data labels are metric-specific (for example, day coverage is rated separately from message volume).",
|
||||
"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, identifier, person = _request_scope(request, "GET")
|
||||
|
||||
Reference in New Issue
Block a user