Further improve detail display and work on inline latency display
This commit is contained in:
@@ -1,19 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone as dt_timezone
|
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
|
|
||||||
from core.models import ChatSession, Message, PersonIdentifier
|
from core.models import ChatSession, Message, PersonIdentifier, WorkspaceConversation
|
||||||
from core.realtime.typing_state import get_person_typing_state
|
from core.realtime.typing_state import get_person_typing_state
|
||||||
from core.views.compose import (
|
from core.views.compose import (
|
||||||
COMPOSE_WS_TOKEN_SALT,
|
COMPOSE_WS_TOKEN_SALT,
|
||||||
_image_urls_from_text,
|
_serialize_messages_with_artifacts,
|
||||||
_is_url_only_text,
|
|
||||||
_looks_like_image_url,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,39 +21,6 @@ def _safe_int(value, default=0):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _fmt_ts(ts_value):
|
|
||||||
try:
|
|
||||||
dt = datetime.fromtimestamp(int(ts_value) / 1000, tz=dt_timezone.utc)
|
|
||||||
return dt.strftime("%H:%M")
|
|
||||||
except Exception:
|
|
||||||
return str(ts_value or "")
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_message(msg):
|
|
||||||
author = str(msg.custom_author or "").strip()
|
|
||||||
text_value = str(msg.text or "")
|
|
||||||
image_urls = _image_urls_from_text(text_value)
|
|
||||||
image_url = image_urls[0] if image_urls else ""
|
|
||||||
hide_text = bool(
|
|
||||||
image_urls
|
|
||||||
and _is_url_only_text(text_value)
|
|
||||||
and all(_looks_like_image_url(url) for url in image_urls)
|
|
||||||
)
|
|
||||||
display_text = text_value if text_value.strip() else ("(no text)" if not image_url else "")
|
|
||||||
return {
|
|
||||||
"id": str(msg.id),
|
|
||||||
"ts": int(msg.ts or 0),
|
|
||||||
"display_ts": _fmt_ts(msg.ts),
|
|
||||||
"text": text_value,
|
|
||||||
"display_text": display_text,
|
|
||||||
"image_url": image_url,
|
|
||||||
"image_urls": image_urls,
|
|
||||||
"hide_text": hide_text,
|
|
||||||
"author": author,
|
|
||||||
"outgoing": author.upper() in {"USER", "BOT"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
||||||
person_identifier = None
|
person_identifier = None
|
||||||
if person_id:
|
if person_id:
|
||||||
@@ -91,11 +55,18 @@ def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
|||||||
"person_id": int(person_identifier.person_id),
|
"person_id": int(person_identifier.person_id),
|
||||||
}
|
}
|
||||||
|
|
||||||
qs = Message.objects.filter(
|
qs = Message.objects.filter(user_id=user_id, session=session).order_by("ts")
|
||||||
|
seed_previous = None
|
||||||
|
if after_ts > 0:
|
||||||
|
seed_previous = (
|
||||||
|
Message.objects.filter(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
session=session,
|
session=session,
|
||||||
).order_by("ts")
|
ts__lte=after_ts,
|
||||||
if after_ts > 0:
|
)
|
||||||
|
.order_by("-ts")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
qs = qs.filter(ts__gt=after_ts)
|
qs = qs.filter(ts__gt=after_ts)
|
||||||
|
|
||||||
rows = list(qs[: max(10, min(limit, 200))])
|
rows = list(qs[: max(10, min(limit, 200))])
|
||||||
@@ -105,8 +76,30 @@ def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
|||||||
.values_list("ts", flat=True)
|
.values_list("ts", flat=True)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
conversation = (
|
||||||
|
WorkspaceConversation.objects.filter(
|
||||||
|
user_id=user_id,
|
||||||
|
participants__id=person_identifier.person_id,
|
||||||
|
)
|
||||||
|
.order_by("-last_event_ts", "-created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
counterpart_identifiers = {
|
||||||
|
str(value or "").strip()
|
||||||
|
for value in PersonIdentifier.objects.filter(
|
||||||
|
user_id=user_id,
|
||||||
|
person_id=person_identifier.person_id,
|
||||||
|
).values_list("identifier", flat=True)
|
||||||
|
if str(value or "").strip()
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"messages": [_serialize_message(row) for row in rows],
|
"messages": _serialize_messages_with_artifacts(
|
||||||
|
rows,
|
||||||
|
counterpart_identifiers=counterpart_identifiers,
|
||||||
|
conversation=conversation,
|
||||||
|
seed_previous=seed_previous,
|
||||||
|
),
|
||||||
"last_ts": int(newest or after_ts or 0),
|
"last_ts": int(newest or after_ts or 0),
|
||||||
"person_id": int(person_identifier.person_id),
|
"person_id": int(person_identifier.person_id),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,24 +296,6 @@
|
|||||||
Queue
|
Queue
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
|
||||||
<a class="navbar-link">
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="navbar-dropdown">
|
|
||||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
|
||||||
Security
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
|
||||||
Notifications
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="{% url 'ais' type='page' %}">
|
|
||||||
AI
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link">
|
<a class="navbar-link">
|
||||||
OSINT
|
OSINT
|
||||||
@@ -333,6 +315,31 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a class="navbar-item add-button">
|
||||||
|
Install
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="navbar-link">
|
||||||
|
Services
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="navbar-dropdown">
|
||||||
|
<a class="navbar-item" href="{% url 'signal' %}">
|
||||||
|
Signal
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" href="{% url 'whatsapp' %}">
|
||||||
|
WhatsApp
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" href="{% url 'instagram' %}">
|
||||||
|
Instagram
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link">
|
<a class="navbar-link">
|
||||||
@@ -348,28 +355,23 @@
|
|||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link">
|
<a class="navbar-link">
|
||||||
Services
|
Settings
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
<a class="navbar-item" href="{% url 'signal' %}">
|
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||||
Signal
|
Security
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" href="{% url 'whatsapp' %}">
|
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||||
WhatsApp
|
Notifications
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" href="{% url 'instagram' %}">
|
<a class="navbar-item" href="{% url 'ais' type='page' %}">
|
||||||
Instagram
|
AI
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="navbar-item add-button">
|
|
||||||
Install
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
|
|||||||
165
core/templates/mixins/window-content/persona-form.html
Normal file
165
core/templates/mixins/window-content/persona-form.html
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
{% include 'mixins/partials/notify.html' %}
|
||||||
|
{% if page_title is not None %}
|
||||||
|
<h1 class="title is-4">{{ page_title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_subtitle is not None %}
|
||||||
|
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.persona-tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-tab-pane.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-tab-pane-body {
|
||||||
|
max-height: min(55vh, 34rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="persona-form-tabs"
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-post="{{ submit_url }}"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for hidden in form.hidden_fields %}
|
||||||
|
{{ hidden }}
|
||||||
|
{% endfor %}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<article class="message is-danger">
|
||||||
|
<div class="message-body">{{ form.non_field_errors }}</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0.75rem;">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="#" data-persona-tab-target="identity">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-id-card"></i></span>
|
||||||
|
<span>Identity</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-persona-tab-target="expression">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-comments"></i></span>
|
||||||
|
<span>Expression</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-persona-tab-target="narrative">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-book-open"></i></span>
|
||||||
|
<span>Narrative</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-persona-tab-target="strategy">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-chess-knight"></i></span>
|
||||||
|
<span>Strategy</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-persona-tab-target="tuning">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-sliders"></i></span>
|
||||||
|
<span>Tuning</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="persona-tab-pane is-active" data-persona-tab-pane="identity">
|
||||||
|
<div class="persona-tab-pane-body">
|
||||||
|
{{ form.alias|as_crispy_field }}
|
||||||
|
{{ form.mbti|as_crispy_field }}
|
||||||
|
{{ form.mbti_identity|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="persona-tab-pane" data-persona-tab-pane="expression">
|
||||||
|
<div class="persona-tab-pane-body">
|
||||||
|
{{ form.communication_style|as_crispy_field }}
|
||||||
|
{{ form.tone|as_crispy_field }}
|
||||||
|
{{ form.humor_style|as_crispy_field }}
|
||||||
|
{{ form.flirting_style|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="persona-tab-pane" data-persona-tab-pane="narrative">
|
||||||
|
<div class="persona-tab-pane-body">
|
||||||
|
{{ form.inner_story|as_crispy_field }}
|
||||||
|
{{ form.core_values|as_crispy_field }}
|
||||||
|
{{ form.likes|as_crispy_field }}
|
||||||
|
{{ form.dislikes|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="persona-tab-pane" data-persona-tab-pane="strategy">
|
||||||
|
<div class="persona-tab-pane-body">
|
||||||
|
{{ form.response_tactics|as_crispy_field }}
|
||||||
|
{{ form.persuasion_tactics|as_crispy_field }}
|
||||||
|
{{ form.boundaries|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="persona-tab-pane" data-persona-tab-pane="tuning">
|
||||||
|
<div class="persona-tab-pane-body">
|
||||||
|
{{ form.trust|as_crispy_field }}
|
||||||
|
{{ form.adaptability|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons" style="margin-top: 1rem; margin-bottom: 0;">
|
||||||
|
{% if hide_cancel is not True %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-light modal-close-button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="button modal-close-button">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const root = document.getElementById("persona-form-tabs");
|
||||||
|
if (!root || root.dataset.tabsBound === "1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.dataset.tabsBound = "1";
|
||||||
|
|
||||||
|
const tabs = Array.from(root.querySelectorAll("[data-persona-tab-target]"));
|
||||||
|
const panes = Array.from(root.querySelectorAll("[data-persona-tab-pane]"));
|
||||||
|
|
||||||
|
function setActive(key) {
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
const li = tab.closest("li");
|
||||||
|
if (!li) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
li.classList.toggle("is-active", tab.dataset.personaTabTarget === key);
|
||||||
|
});
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
pane.classList.toggle("is-active", pane.dataset.personaTabPane === key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setActive(tab.dataset.personaTabTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabs[0]) {
|
||||||
|
setActive(tabs[0].dataset.personaTabTarget);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -14,16 +14,7 @@
|
|||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<h1 class="title is-4" style="margin-bottom: 0.35rem;">Information: {{ person.name }}</h1>
|
<h1 class="title is-4" style="margin-bottom: 0.35rem;">Information: {{ person.name }}</h1>
|
||||||
<p class="is-size-7 has-text-grey">Commitment directionality and underlying metric factors.</p>
|
<p class="is-size-7 has-text-grey">Commitment directionality and underlying metric factors.</p>
|
||||||
<div class="tags has-addons" style="margin-top: 0.6rem;">
|
{% include "partials/ai-insight-nav.html" with active_tab="information" %}
|
||||||
<a class="tag is-link is-light" href="{{ graphs_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
|
||||||
<span>Insight Graphs</span>
|
|
||||||
</a>
|
|
||||||
<a class="tag is-dark" href="#">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
|
||||||
<span>Data View</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
@@ -41,16 +32,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons are-small">
|
|
||||||
<a class="button is-light" href="{{ graphs_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
|
||||||
<span>All Graphs</span>
|
|
||||||
</a>
|
|
||||||
<a class="button is-light" href="{{ help_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
|
||||||
<span>Scoring Help</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<h1 class="title is-4" style="margin-bottom: 0.35rem;">{{ metric.title }}: {{ person.name }}</h1>
|
<h1 class="title is-4" style="margin-bottom: 0.35rem;">{{ metric.title }}: {{ person.name }}</h1>
|
||||||
<p class="is-size-7 has-text-grey">Conversation {{ workspace_conversation.id }}</p>
|
<p class="is-size-7 has-text-grey">Conversation {{ workspace_conversation.id }}</p>
|
||||||
|
{% include "partials/ai-insight-nav.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -33,20 +34,6 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
|
||||||
<a class="button is-light" href="{{ graphs_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
|
||||||
<span>All Graphs</span>
|
|
||||||
</a>
|
|
||||||
<a class="button is-light" href="{{ information_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
|
||||||
<span>Information</span>
|
|
||||||
</a>
|
|
||||||
<a class="button is-light" href="{{ help_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
|
||||||
<span>Scoring Help</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|||||||
@@ -16,16 +16,7 @@
|
|||||||
<p class="is-size-7 has-text-grey">
|
<p class="is-size-7 has-text-grey">
|
||||||
Historical metrics for workspace {{ workspace_conversation.id }}
|
Historical metrics for workspace {{ workspace_conversation.id }}
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons are-small" style="margin-top: 0.6rem;">
|
{% include "partials/ai-insight-nav.html" with active_tab="graphs" %}
|
||||||
<a class="button is-light" href="{{ information_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
|
||||||
<span>Information</span>
|
|
||||||
</a>
|
|
||||||
<a class="button is-light" href="{{ help_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
|
||||||
<span>Scoring Help</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for graph in graph_cards %}
|
{% for graph in graph_cards %}
|
||||||
|
|||||||
@@ -17,16 +17,7 @@
|
|||||||
Combined explanation for each metric collection group and what it can
|
Combined explanation for each metric collection group and what it can
|
||||||
imply in relationship dynamics.
|
imply in relationship dynamics.
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons are-small" style="margin-top: 0.6rem;">
|
{% include "partials/ai-insight-nav.html" with active_tab="help" %}
|
||||||
<a class="button is-light" href="{{ graphs_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
|
||||||
<span>Insight Graphs</span>
|
|
||||||
</a>
|
|
||||||
<a class="button is-light" href="{{ information_url }}">
|
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
|
||||||
<span>Information</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for group_key, group in groups.items %}
|
{% for group_key, group in groups.items %}
|
||||||
|
|||||||
20
core/templates/partials/ai-insight-nav.html
Normal file
20
core/templates/partials/ai-insight-nav.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<div class="tags has-addons" style="margin-top: 0.6rem; margin-bottom: 0;">
|
||||||
|
<a
|
||||||
|
class="tag {% if active_tab == 'graphs' %}is-dark{% else %}is-link is-light{% endif %}"
|
||||||
|
href="{{ graphs_url }}">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||||
|
<span>Insight Graphs</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="tag {% if active_tab == 'information' %}is-dark{% else %}is-link is-light{% endif %}"
|
||||||
|
href="{{ information_url }}">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
||||||
|
<span>Information View</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="tag {% if active_tab == 'help' %}is-dark{% else %}is-link is-light{% endif %}"
|
||||||
|
href="{{ help_url }}">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
||||||
|
<span>Scoring Guide</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -188,13 +188,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if memory_proposals %}
|
{% if memory_proposals %}
|
||||||
<article class="box" style="padding: 0.55rem; margin-top: 0.55rem; border: 1px solid rgba(0, 0, 0, 0.14); box-shadow: none;">
|
<section style="margin-top: 0.55rem;">
|
||||||
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Memory Proposals</p>
|
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Memory Proposals</p>
|
||||||
{% if memory_proposal_groups %}
|
{% if memory_proposal_groups %}
|
||||||
<div class="columns is-multiline" style="margin: 0 -0.25rem;">
|
<div class="columns is-multiline" style="margin: 0 -0.25rem;">
|
||||||
{% for group in memory_proposal_groups %}
|
{% for group in memory_proposal_groups %}
|
||||||
<div class="column is-12-mobile is-6-tablet" style="padding: 0.25rem;">
|
<div class="column is-12-mobile is-6-tablet" style="padding: 0.25rem;">
|
||||||
<article class="box" style="padding: 0.45rem; margin-bottom: 0; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;">
|
|
||||||
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.3rem;">{{ group.title }}</p>
|
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.3rem;">{{ group.title }}</p>
|
||||||
<ul style="margin: 0 0 0.25rem 1.1rem;">
|
<ul style="margin: 0 0 0.25rem 1.1rem;">
|
||||||
{% for proposal in group.items %}
|
{% for proposal in group.items %}
|
||||||
@@ -203,7 +202,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +212,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</article>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if citations %}
|
{% if citations %}
|
||||||
|
|||||||
@@ -63,29 +63,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0.55rem;">
|
|
||||||
<ul>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-plan_board" class="is-active">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'plan_board'); return false;">Rules & Games</a>
|
|
||||||
</li>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-corrections">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'corrections'); return false;">Corrections</a>
|
|
||||||
</li>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-engage">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'engage'); return false;">Engage</a>
|
|
||||||
</li>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-fundamentals">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'fundamentals'); return false;">Fundamentals</a>
|
|
||||||
</li>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-auto">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'auto'); return false;">Auto</a>
|
|
||||||
</li>
|
|
||||||
<li id="mitigation-tab-btn-{{ person.id }}-ask_ai">
|
|
||||||
<a onclick="giaMitigationShowTab('{{ person.id }}', 'ask_ai'); return false;">Ask AI</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="mitigation-tab-{{ person.id }}-plan_board" class="mitigation-tab-pane">
|
<div id="mitigation-tab-{{ person.id }}-plan_board" class="mitigation-tab-pane">
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center" style="gap: 0.5rem; margin-bottom: 0.45rem; flex-wrap: wrap;">
|
<div class="is-flex is-justify-content-space-between is-align-items-center" style="gap: 0.5rem; margin-bottom: 0.45rem; flex-wrap: wrap;">
|
||||||
<p class="is-size-7">Two lanes by type: rules on the left, games on the right.</p>
|
<p class="is-size-7">Two lanes by type: rules on the left, games on the right.</p>
|
||||||
@@ -685,10 +662,12 @@
|
|||||||
["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"].forEach(function(name) {
|
["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"].forEach(function(name) {
|
||||||
const pane = document.getElementById("mitigation-tab-" + personId + "-" + name);
|
const pane = document.getElementById("mitigation-tab-" + personId + "-" + name);
|
||||||
const tab = document.getElementById("mitigation-tab-btn-" + personId + "-" + name);
|
const tab = document.getElementById("mitigation-tab-btn-" + personId + "-" + name);
|
||||||
if (!pane || !tab) return;
|
if (!pane) return;
|
||||||
const active = name === tabName;
|
const active = name === tabName;
|
||||||
pane.style.display = active ? "block" : "none";
|
pane.style.display = active ? "block" : "none";
|
||||||
|
if (tab) {
|
||||||
tab.classList.toggle("is-active", active);
|
tab.classList.toggle("is-active", active);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setActiveTabHiddenFields(tabName);
|
setActiveTabHiddenFields(tabName);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||||
<span>Insight Graphs</span>
|
<span>Insight Graphs</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="button is-light" href="{% url 'ai_workspace_information' type='page' person_id=person.id %}">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
||||||
|
<span>Information</span>
|
||||||
|
</a>
|
||||||
<a class="button is-light" href="{% url 'ai_workspace_insight_help' type='page' person_id=person.id %}">
|
<a class="button is-light" href="{% url 'ai_workspace_insight_help' type='page' person_id=person.id %}">
|
||||||
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
|
||||||
<span>Scoring Help</span>
|
<span>Scoring Help</span>
|
||||||
@@ -38,19 +42,33 @@
|
|||||||
</div>
|
</div>
|
||||||
{% with participants=workspace_conversation.participants.all %}
|
{% with participants=workspace_conversation.participants.all %}
|
||||||
{% if participants %}
|
{% if participants %}
|
||||||
<p class="is-size-7" style="margin-top: 0.35rem; margin-bottom: 0;">
|
<div style="margin-top: 0.35rem;">
|
||||||
Participants:
|
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.2rem;">Participants</p>
|
||||||
|
<div class="tags">
|
||||||
{% for participant in participants %}
|
{% for participant in participants %}
|
||||||
{% if not forloop.first %}, {% endif %}
|
<span class="tag is-light">{{ participant.name }}</span>
|
||||||
{{ participant.name }}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% if workspace_conversation.participant_feedback %}
|
{% if participant_feedback_display %}
|
||||||
<p class="is-size-7" style="margin-top: 0.35rem; margin-bottom: 0;">
|
<div style="margin-top: 0.25rem;">
|
||||||
Participant Feedback: {{ workspace_conversation.participant_feedback }}
|
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.2rem;">Participant Feedback</p>
|
||||||
</p>
|
<div class="tags">
|
||||||
|
<span class="tag is-light {{ participant_feedback_display.state_class }}">
|
||||||
|
<span class="icon is-small"><i class="{{ participant_feedback_display.state_icon }}"></i></span>
|
||||||
|
<span>{{ participant_feedback_display.state_label }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="tag is-light">Inbound {{ participant_feedback_display.inbound_messages|default:"-" }}</span>
|
||||||
|
<span class="tag is-light">Outbound {{ participant_feedback_display.outbound_messages|default:"-" }}</span>
|
||||||
|
<span class="tag is-light">Sample Msg {{ participant_feedback_display.sample_messages|default:"-" }}</span>
|
||||||
|
<span class="tag is-light">Sample Days {{ participant_feedback_display.sample_days|default:"-" }}</span>
|
||||||
|
{% if participant_feedback_display.updated_at_label %}
|
||||||
|
<span class="tag is-light">Updated {{ participant_feedback_display.updated_at_label }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if compose_page_url %}
|
{% if compose_page_url %}
|
||||||
<div class="buttons are-small" style="margin-top: 0.45rem; margin-bottom: 0;">
|
<div class="buttons are-small" style="margin-top: 0.45rem; margin-bottom: 0;">
|
||||||
@@ -96,17 +114,32 @@
|
|||||||
<div class="is-flex is-justify-content-space-between is-align-items-center" style="margin-bottom: 0.4rem;">
|
<div class="is-flex is-justify-content-space-between is-align-items-center" style="margin-bottom: 0.4rem;">
|
||||||
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0;">
|
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0;">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="ai-tab-{{ person.id }}-artifacts">
|
<li id="ai-tab-{{ person.id }}-plan_board" class="is-active">
|
||||||
<a onclick="giaWorkspaceRun('{{ person.id }}', 'artifacts', false); return false;">Plan</a>
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'plan_board', false); return false;">Plan</a>
|
||||||
|
</li>
|
||||||
|
<li id="ai-tab-{{ person.id }}-corrections">
|
||||||
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'corrections', false); return false;">Corrections</a>
|
||||||
|
</li>
|
||||||
|
<li id="ai-tab-{{ person.id }}-engage">
|
||||||
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'engage', false); return false;">Engage</a>
|
||||||
|
</li>
|
||||||
|
<li id="ai-tab-{{ person.id }}-fundamentals">
|
||||||
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'fundamentals', false); return false;">Fundamentals</a>
|
||||||
|
</li>
|
||||||
|
<li id="ai-tab-{{ person.id }}-auto">
|
||||||
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'auto', false); return false;">Auto</a>
|
||||||
|
</li>
|
||||||
|
<li id="ai-tab-{{ person.id }}-ask_ai">
|
||||||
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'ask_ai', false); return false;">Ask AI</a>
|
||||||
</li>
|
</li>
|
||||||
<li id="ai-tab-{{ person.id }}-summarise">
|
<li id="ai-tab-{{ person.id }}-summarise">
|
||||||
<a onclick="giaWorkspaceRun('{{ person.id }}', 'summarise', false); return false;">Summary</a>
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'summarise', false); return false;">Summary</a>
|
||||||
</li>
|
</li>
|
||||||
<li id="ai-tab-{{ person.id }}-draft_reply" class="is-active">
|
<li id="ai-tab-{{ person.id }}-draft_reply">
|
||||||
<a onclick="giaWorkspaceRun('{{ person.id }}', 'draft_reply', false); return false;">Draft</a>
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'draft_reply', false); return false;">Draft</a>
|
||||||
</li>
|
</li>
|
||||||
<li id="ai-tab-{{ person.id }}-extract_patterns">
|
<li id="ai-tab-{{ person.id }}-extract_patterns">
|
||||||
<a onclick="giaWorkspaceRun('{{ person.id }}', 'extract_patterns', false); return false;">Patterns</a>
|
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'extract_patterns', false); return false;">Patterns</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +196,8 @@
|
|||||||
// One-time migration flush to avoid stale cached pane HTML from earlier UI schema.
|
// One-time migration flush to avoid stale cached pane HTML from earlier UI schema.
|
||||||
localStorage.removeItem("gia_workspace_cache_v1");
|
localStorage.removeItem("gia_workspace_cache_v1");
|
||||||
localStorage.removeItem("gia_workspace_cache_v2");
|
localStorage.removeItem("gia_workspace_cache_v2");
|
||||||
return JSON.parse(localStorage.getItem("gia_workspace_cache_v3") || "{}");
|
localStorage.removeItem("gia_workspace_cache_v3");
|
||||||
|
return JSON.parse(localStorage.getItem("gia_workspace_cache_v4") || "{}");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -171,7 +205,7 @@
|
|||||||
|
|
||||||
function persistCache() {
|
function persistCache() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("gia_workspace_cache_v3", JSON.stringify(window.giaWorkspaceCache));
|
localStorage.setItem("gia_workspace_cache_v4", JSON.stringify(window.giaWorkspaceCache));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore storage write issues.
|
// Ignore storage write issues.
|
||||||
}
|
}
|
||||||
@@ -282,6 +316,46 @@
|
|||||||
indicator.style.display = show ? "inline-flex" : "none";
|
indicator.style.display = show ? "inline-flex" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OPERATION_TABS = ["summarise", "draft_reply", "extract_patterns"];
|
||||||
|
const MITIGATION_TABS = ["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"];
|
||||||
|
const ALL_TOP_TABS = MITIGATION_TABS.concat(OPERATION_TABS);
|
||||||
|
|
||||||
|
function isMitigationTab(tabKey) {
|
||||||
|
return MITIGATION_TABS.indexOf(tabKey) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function operationForTab(tabKey) {
|
||||||
|
return isMitigationTab(tabKey) ? "artifacts" : tabKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTopCapsuleActive(tabKey) {
|
||||||
|
ALL_TOP_TABS.forEach(function(name) {
|
||||||
|
const tab = document.getElementById("ai-tab-" + personId + "-" + name);
|
||||||
|
if (tab) {
|
||||||
|
tab.classList.toggle("is-active", name === tabKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOperationPane(operation) {
|
||||||
|
["artifacts", "summarise", "draft_reply", "extract_patterns"].forEach(function(op) {
|
||||||
|
const pane = document.getElementById("ai-pane-" + personId + "-" + op);
|
||||||
|
if (!pane) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pane.style.display = op === operation ? "block" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMitigationTabSelection() {
|
||||||
|
const state = window.giaWorkspaceState[personId] || {};
|
||||||
|
const targetTab = state.currentMitigationTab || "plan_board";
|
||||||
|
setTopCapsuleActive(targetTab);
|
||||||
|
if (typeof window.giaMitigationShowTab === "function") {
|
||||||
|
window.giaMitigationShowTab(personId, targetTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hydrateCachedIfAvailable(operation) {
|
function hydrateCachedIfAvailable(operation) {
|
||||||
if (operation === "artifacts") {
|
if (operation === "artifacts") {
|
||||||
return false;
|
return false;
|
||||||
@@ -303,29 +377,35 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.giaWorkspaceShowTab = function(pid, operation) {
|
window.giaWorkspaceShowTab = function(pid, operation, tabKey) {
|
||||||
if (pid !== personId) {
|
if (pid !== personId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
["artifacts", "summarise", "draft_reply", "extract_patterns"].forEach(function(op) {
|
showOperationPane(operation);
|
||||||
const tab = document.getElementById("ai-tab-" + personId + "-" + op);
|
const activeTab = tabKey || (
|
||||||
const pane = document.getElementById("ai-pane-" + personId + "-" + op);
|
operation === "artifacts"
|
||||||
if (!tab || !pane) {
|
? ((window.giaWorkspaceState[personId] || {}).currentMitigationTab || "plan_board")
|
||||||
return;
|
: operation
|
||||||
}
|
);
|
||||||
if (op === operation) {
|
setTopCapsuleActive(activeTab);
|
||||||
tab.classList.add("is-active");
|
|
||||||
pane.style.display = "block";
|
|
||||||
} else {
|
|
||||||
tab.classList.remove("is-active");
|
|
||||||
pane.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const hydrated = hydrateCachedIfAvailable(operation);
|
const hydrated = hydrateCachedIfAvailable(operation);
|
||||||
const entry = operation === "artifacts" ? null : getCacheEntry(operation);
|
const entry = operation === "artifacts" ? null : getCacheEntry(operation);
|
||||||
setCachedIndicator(hydrated || !!entry, entry ? entry.ts : null);
|
setCachedIndicator(hydrated || !!entry, entry ? entry.ts : null);
|
||||||
window.giaWorkspaceState[personId] = window.giaWorkspaceState[personId] || {};
|
window.giaWorkspaceState[personId] = window.giaWorkspaceState[personId] || {};
|
||||||
window.giaWorkspaceState[personId].current = operation;
|
window.giaWorkspaceState[personId].current = operation;
|
||||||
|
window.giaWorkspaceState[personId].currentTab = activeTab;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.giaWorkspaceOpenTab = function(pid, tabKey, forceRefresh) {
|
||||||
|
if (pid !== personId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.giaWorkspaceState[personId] = window.giaWorkspaceState[personId] || {};
|
||||||
|
if (isMitigationTab(tabKey)) {
|
||||||
|
window.giaWorkspaceState[personId].currentMitigationTab = tabKey;
|
||||||
|
}
|
||||||
|
window.giaWorkspaceState[personId].pendingTabKey = tabKey;
|
||||||
|
window.giaWorkspaceRun(personId, operationForTab(tabKey), !!forceRefresh);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.giaWorkspaceRun = function(pid, operation, forceRefresh) {
|
window.giaWorkspaceRun = function(pid, operation, forceRefresh) {
|
||||||
@@ -339,11 +419,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentState = window.giaWorkspaceState[personId] || {};
|
const currentState = window.giaWorkspaceState[personId] || {};
|
||||||
|
const targetTabKey = currentState.pendingTabKey || (
|
||||||
|
operation === "artifacts"
|
||||||
|
? (currentState.currentMitigationTab || "plan_board")
|
||||||
|
: operation
|
||||||
|
);
|
||||||
if (!forceRefresh && currentState.current === operation && pane.dataset.loaded === "1") {
|
if (!forceRefresh && currentState.current === operation && pane.dataset.loaded === "1") {
|
||||||
window.giaWorkspaceShowTab(personId, operation);
|
window.giaWorkspaceShowTab(personId, operation, targetTabKey);
|
||||||
|
if (operation === "artifacts") {
|
||||||
|
applyMitigationTabSelection();
|
||||||
|
}
|
||||||
|
if (window.giaWorkspaceState[personId]) {
|
||||||
|
window.giaWorkspaceState[personId].pendingTabKey = "";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.giaWorkspaceShowTab(personId, operation);
|
window.giaWorkspaceShowTab(personId, operation, targetTabKey);
|
||||||
|
if (operation === "artifacts") {
|
||||||
|
applyMitigationTabSelection();
|
||||||
|
}
|
||||||
|
|
||||||
const key = cacheKey(operation);
|
const key = cacheKey(operation);
|
||||||
const entry = getCacheEntry(operation);
|
const entry = getCacheEntry(operation);
|
||||||
@@ -360,6 +454,12 @@
|
|||||||
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
|
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
|
||||||
window.giaWorkspaceUseDraft(personId, operation, 0);
|
window.giaWorkspaceUseDraft(personId, operation, 0);
|
||||||
}
|
}
|
||||||
|
if (operation === "artifacts") {
|
||||||
|
applyMitigationTabSelection();
|
||||||
|
}
|
||||||
|
if (window.giaWorkspaceState[personId]) {
|
||||||
|
window.giaWorkspaceState[personId].pendingTabKey = "";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +491,12 @@
|
|||||||
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
|
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
|
||||||
window.giaWorkspaceUseDraft(personId, operation, 0);
|
window.giaWorkspaceUseDraft(personId, operation, 0);
|
||||||
}
|
}
|
||||||
|
if (operation === "artifacts") {
|
||||||
|
applyMitigationTabSelection();
|
||||||
|
}
|
||||||
|
if (window.giaWorkspaceState[personId]) {
|
||||||
|
window.giaWorkspaceState[personId].pendingTabKey = "";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
pane.innerHTML = '<div class="notification is-danger is-light ai-animate-in">Failed to load AI response.</div>';
|
pane.innerHTML = '<div class="notification is-danger is-light ai-animate-in">Failed to load AI response.</div>';
|
||||||
@@ -401,8 +507,13 @@
|
|||||||
if (pid !== personId) {
|
if (pid !== personId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const current = (window.giaWorkspaceState[personId] && window.giaWorkspaceState[personId].current) || "summarise";
|
const state = window.giaWorkspaceState[personId] || {};
|
||||||
window.giaWorkspaceRun(personId, current, true);
|
const currentTab = state.currentTab || (
|
||||||
|
state.current === "artifacts"
|
||||||
|
? (state.currentMitigationTab || "plan_board")
|
||||||
|
: (state.current || "plan_board")
|
||||||
|
);
|
||||||
|
window.giaWorkspaceOpenTab(personId, currentTab, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.giaWorkspaceUseDraft = function(pid, operation, index) {
|
window.giaWorkspaceUseDraft = function(pid, operation, index) {
|
||||||
@@ -503,12 +614,14 @@
|
|||||||
names.forEach(function(name) {
|
names.forEach(function(name) {
|
||||||
const pane = document.getElementById("mitigation-tab-" + pid + "-" + name);
|
const pane = document.getElementById("mitigation-tab-" + pid + "-" + name);
|
||||||
const tab = document.getElementById("mitigation-tab-btn-" + pid + "-" + name);
|
const tab = document.getElementById("mitigation-tab-btn-" + pid + "-" + name);
|
||||||
if (!pane || !tab) {
|
if (!pane) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const active = (name === tabName);
|
const active = (name === tabName);
|
||||||
pane.style.display = active ? "block" : "none";
|
pane.style.display = active ? "block" : "none";
|
||||||
|
if (tab) {
|
||||||
tab.classList.toggle("is-active", active);
|
tab.classList.toggle("is-active", active);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const shell = document.getElementById("mitigation-shell-" + pid);
|
const shell = document.getElementById("mitigation-shell-" + pid);
|
||||||
if (!shell) {
|
if (!shell) {
|
||||||
@@ -584,6 +697,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
window.giaWorkspaceRun(personId, "artifacts", false);
|
window.giaWorkspaceOpenTab(personId, "plan_board", false);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -121,6 +121,25 @@
|
|||||||
data-engage-send-url="{{ compose_engage_send_url }}">
|
data-engage-send-url="{{ compose_engage_send_url }}">
|
||||||
{% for msg in serialized_messages %}
|
{% for msg in serialized_messages %}
|
||||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}">
|
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}">
|
||||||
|
{% if msg.gap_fragments %}
|
||||||
|
<div class="compose-gap-artifacts">
|
||||||
|
{% for frag in msg.gap_fragments %}
|
||||||
|
<article class="compose-artifact compose-artifact-gap">
|
||||||
|
<p class="compose-artifact-head">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-hourglass-half"></i></span>
|
||||||
|
<span>{{ frag.focus }} · {{ frag.lag }}</span>
|
||||||
|
<span class="compose-artifact-score">Score {{ frag.score }}</span>
|
||||||
|
</p>
|
||||||
|
{% if frag.calculation %}
|
||||||
|
<p class="compose-artifact-detail">How: {{ frag.calculation }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if frag.psychology %}
|
||||||
|
<p class="compose-artifact-detail">Meaning: {{ frag.psychology }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
|
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
|
||||||
{% if msg.image_urls %}
|
{% if msg.image_urls %}
|
||||||
{% for image_url in msg.image_urls %}
|
{% for image_url in msg.image_urls %}
|
||||||
@@ -152,6 +171,21 @@
|
|||||||
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
{% if msg.metric_fragments %}
|
||||||
|
<div class="compose-metric-artifacts">
|
||||||
|
{% for frag in msg.metric_fragments %}
|
||||||
|
<article
|
||||||
|
class="compose-artifact compose-artifact-metric"
|
||||||
|
title="How it is calculated: {{ frag.calculation }}{% if frag.psychology %} | Psychological interpretation: {{ frag.psychology }}{% endif %}">
|
||||||
|
<p class="compose-artifact-head">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||||
|
<span>{{ frag.title }}</span>
|
||||||
|
<span class="compose-artifact-score">{{ frag.value }}</span>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="compose-empty">No stored messages for this contact yet.</p>
|
<p class="compose-empty">No stored messages for this contact yet.</p>
|
||||||
@@ -219,13 +253,15 @@
|
|||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-row {
|
#{{ panel_id }} .compose-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-row.is-in {
|
#{{ panel_id }} .compose-row.is-in {
|
||||||
justify-content: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-row.is-out {
|
#{{ panel_id }} .compose-row.is-out {
|
||||||
justify-content: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-bubble {
|
#{{ panel_id }} .compose-bubble {
|
||||||
max-width: min(85%, 46rem);
|
max-width: min(85%, 46rem);
|
||||||
@@ -265,6 +301,49 @@
|
|||||||
#{{ panel_id }} .compose-msg-meta {
|
#{{ panel_id }} .compose-msg-meta {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
#{{ panel_id }} .compose-gap-artifacts {
|
||||||
|
align-self: center;
|
||||||
|
width: min(92%, 34rem);
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-metric-artifacts {
|
||||||
|
width: min(86%, 46rem);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(9.4rem, 1fr));
|
||||||
|
gap: 0.28rem;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact {
|
||||||
|
border: 1px dashed rgba(0, 0, 0, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(252, 253, 255, 0.96);
|
||||||
|
padding: 0.28rem 0.38rem;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact.compose-artifact-gap {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact-head {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
align-items: center;
|
||||||
|
color: #3f4f67;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact-head .icon {
|
||||||
|
color: #6a88b4;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact-score {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #2f4f7a;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-artifact-detail {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
color: #637185;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
#{{ panel_id }} .compose-empty {
|
#{{ panel_id }} .compose-empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #6f6f6f;
|
color: #6f6f6f;
|
||||||
@@ -456,6 +535,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.35rem 0.42rem;
|
padding: 0.35rem 0.42rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-qi-chip p {
|
#{{ panel_id }} .compose-qi-chip p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -468,9 +548,22 @@
|
|||||||
#{{ panel_id }} .compose-qi-chip .v {
|
#{{ panel_id }} .compose-qi-chip .v {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.28rem;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
#{{ panel_id }} .compose-qi-chip .v > span:last-child {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-qi-list {
|
#{{ panel_id }} .compose-qi-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 0.36rem;
|
gap: 0.36rem;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-qi-row {
|
#{{ panel_id }} .compose-qi-row {
|
||||||
@@ -478,6 +571,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 0.42rem 0.46rem;
|
padding: 0.42rem 0.46rem;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
#{{ panel_id }} .compose-qi-row-head {
|
#{{ panel_id }} .compose-qi-row-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -750,6 +844,88 @@
|
|||||||
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
|
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
|
||||||
row.dataset.ts = String(msg.ts || 0);
|
row.dataset.ts = String(msg.ts || 0);
|
||||||
|
|
||||||
|
const appendGapArtifacts = function (fragments) {
|
||||||
|
if (!Array.isArray(fragments) || !fragments.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.className = "compose-gap-artifacts";
|
||||||
|
fragments.forEach(function (fragment) {
|
||||||
|
const artifact = document.createElement("article");
|
||||||
|
artifact.className = "compose-artifact compose-artifact-gap";
|
||||||
|
const head = document.createElement("p");
|
||||||
|
head.className = "compose-artifact-head";
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "icon is-small";
|
||||||
|
icon.innerHTML = '<i class="fa-solid fa-hourglass-half"></i>';
|
||||||
|
const focus = document.createElement("span");
|
||||||
|
const focusText = String(fragment.focus || "Response gap");
|
||||||
|
const lagText = String(fragment.lag || "");
|
||||||
|
focus.textContent = lagText ? (focusText + " · " + lagText) : focusText;
|
||||||
|
const score = document.createElement("span");
|
||||||
|
score.className = "compose-artifact-score";
|
||||||
|
score.textContent = "Score " + String(fragment.score || "-");
|
||||||
|
head.appendChild(icon);
|
||||||
|
head.appendChild(focus);
|
||||||
|
head.appendChild(score);
|
||||||
|
artifact.appendChild(head);
|
||||||
|
if (fragment.calculation) {
|
||||||
|
const calc = document.createElement("p");
|
||||||
|
calc.className = "compose-artifact-detail";
|
||||||
|
calc.textContent = "How: " + String(fragment.calculation || "");
|
||||||
|
artifact.appendChild(calc);
|
||||||
|
}
|
||||||
|
if (fragment.psychology) {
|
||||||
|
const psych = document.createElement("p");
|
||||||
|
psych.className = "compose-artifact-detail";
|
||||||
|
psych.textContent = "Meaning: " + String(fragment.psychology || "");
|
||||||
|
artifact.appendChild(psych);
|
||||||
|
}
|
||||||
|
wrap.appendChild(artifact);
|
||||||
|
});
|
||||||
|
row.appendChild(wrap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendMetricArtifacts = function (fragments) {
|
||||||
|
if (!Array.isArray(fragments) || !fragments.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.className = "compose-metric-artifacts";
|
||||||
|
fragments.forEach(function (fragment) {
|
||||||
|
const artifact = document.createElement("article");
|
||||||
|
artifact.className = "compose-artifact compose-artifact-metric";
|
||||||
|
const calc = String(fragment.calculation || "");
|
||||||
|
const psych = String(fragment.psychology || "");
|
||||||
|
const tips = [];
|
||||||
|
if (calc) {
|
||||||
|
tips.push("How it is calculated: " + calc);
|
||||||
|
}
|
||||||
|
if (psych) {
|
||||||
|
tips.push("Psychological interpretation: " + psych);
|
||||||
|
}
|
||||||
|
artifact.title = tips.join(" | ");
|
||||||
|
const head = document.createElement("p");
|
||||||
|
head.className = "compose-artifact-head";
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "icon is-small";
|
||||||
|
icon.innerHTML = '<i class="fa-solid fa-chart-line"></i>';
|
||||||
|
const title = document.createElement("span");
|
||||||
|
title.textContent = String(fragment.title || "Metric");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
value.className = "compose-artifact-score";
|
||||||
|
value.textContent = String(fragment.value || "-");
|
||||||
|
head.appendChild(icon);
|
||||||
|
head.appendChild(title);
|
||||||
|
head.appendChild(value);
|
||||||
|
artifact.appendChild(head);
|
||||||
|
wrap.appendChild(artifact);
|
||||||
|
});
|
||||||
|
row.appendChild(wrap);
|
||||||
|
};
|
||||||
|
|
||||||
|
appendGapArtifacts(msg.gap_fragments);
|
||||||
|
|
||||||
const bubble = document.createElement("article");
|
const bubble = document.createElement("article");
|
||||||
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
|
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
|
||||||
|
|
||||||
@@ -787,6 +963,7 @@
|
|||||||
bubble.appendChild(meta);
|
bubble.appendChild(meta);
|
||||||
|
|
||||||
row.appendChild(bubble);
|
row.appendChild(bubble);
|
||||||
|
appendMetricArtifacts(msg.metric_fragments);
|
||||||
const empty = thread.querySelector(".compose-empty");
|
const empty = thread.querySelector(".compose-empty");
|
||||||
if (empty) {
|
if (empty) {
|
||||||
empty.remove();
|
empty.remove();
|
||||||
@@ -1144,20 +1321,83 @@
|
|||||||
const docs = Array.isArray(payload.docs) ? payload.docs : [];
|
const docs = Array.isArray(payload.docs) ? payload.docs : [];
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const stateFaceMeta = function (stateText) {
|
||||||
|
const state = String(stateText || "").toLowerCase();
|
||||||
|
if (state.includes("balanced")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-smile",
|
||||||
|
className: "has-text-success",
|
||||||
|
label: "Balanced"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (state.includes("withdrawing")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-frown",
|
||||||
|
className: "has-text-danger",
|
||||||
|
label: "Withdrawing"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (state.includes("overextending")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-meh",
|
||||||
|
className: "has-text-warning",
|
||||||
|
label: "Overextending"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (state.includes("stable")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-smile",
|
||||||
|
className: "has-text-success",
|
||||||
|
label: "Positive"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (state.includes("watch")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-meh",
|
||||||
|
className: "has-text-warning",
|
||||||
|
label: "Mixed"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (state.includes("fragile")) {
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-frown",
|
||||||
|
className: "has-text-danger",
|
||||||
|
label: "Strained"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
icon: "fa-regular fa-face-meh-blank",
|
||||||
|
className: "has-text-grey",
|
||||||
|
label: "Unknown"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const stateFace = stateFaceMeta(summary.state);
|
||||||
|
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
head.className = "compose-qi-head";
|
head.className = "compose-qi-head";
|
||||||
[
|
[
|
||||||
["Platform", summary.platform || "-"],
|
{ key: "Platform", value: summary.platform || "-" },
|
||||||
["State", summary.state || "-"],
|
{
|
||||||
["Data Points", String(summary.snapshot_count || 0)],
|
key: "Participant State",
|
||||||
["Thread", summary.thread || "-"],
|
value: summary.state || "-",
|
||||||
|
icon: stateFace.icon,
|
||||||
|
className: stateFace.className,
|
||||||
|
},
|
||||||
|
{ key: "Data Points", value: String(summary.snapshot_count || 0) },
|
||||||
|
{ key: "Thread", value: summary.thread || "-" },
|
||||||
].forEach(function (pair) {
|
].forEach(function (pair) {
|
||||||
const chip = document.createElement("div");
|
const chip = document.createElement("div");
|
||||||
chip.className = "compose-qi-chip";
|
chip.className = "compose-qi-chip";
|
||||||
chip.innerHTML = (
|
let valueHtml = String(pair.value || "-");
|
||||||
'<p class="k">' + pair[0] + "</p>"
|
if (pair.icon) {
|
||||||
+ '<p class="v">' + pair[1] + "</p>"
|
valueHtml = (
|
||||||
|
'<span class="' + String(pair.className || "") + '">'
|
||||||
|
+ '<span class="icon is-small"><i class="' + String(pair.icon) + '"></i></span>'
|
||||||
|
+ "</span>"
|
||||||
|
+ "<span>" + valueHtml + "</span>"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
chip.innerHTML = '<p class="k">' + pair.key + "</p>" + '<p class="v">' + valueHtml + "</p>";
|
||||||
head.appendChild(chip);
|
head.appendChild(chip);
|
||||||
});
|
});
|
||||||
container.appendChild(head);
|
container.appendChild(head);
|
||||||
@@ -1205,15 +1445,6 @@
|
|||||||
});
|
});
|
||||||
container.appendChild(docsList);
|
container.appendChild(docsList);
|
||||||
}
|
}
|
||||||
const openBtn = document.createElement("a");
|
|
||||||
openBtn.className = "button is-light is-small is-rounded";
|
|
||||||
openBtn.href = "{{ ai_workspace_url }}";
|
|
||||||
openBtn.innerHTML = (
|
|
||||||
'<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>'
|
|
||||||
+ "<span>Open Minimal AI Workspace</span>"
|
|
||||||
);
|
|
||||||
openBtn.style.marginTop = "0.45rem";
|
|
||||||
container.appendChild(openBtn);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCardLoading(card, false);
|
setCardLoading(card, false);
|
||||||
card.querySelector(".compose-ai-content").textContent =
|
card.querySelector(".compose-ai-content").textContent =
|
||||||
|
|||||||
@@ -51,12 +51,46 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="osint-results-meta">
|
||||||
|
<div class="osint-results-meta-left">
|
||||||
|
<div class="dropdown is-hoverable" id="{{ osint_table_id }}-columns-dropdown">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button is-small is-light" aria-haspopup="true" aria-controls="{{ osint_table_id }}-columns-menu">
|
||||||
|
<span>Show/Hide Fields</span>
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-angle-down"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="{{ osint_table_id }}-columns-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
{% for column in osint_columns %}
|
||||||
|
<a
|
||||||
|
class="dropdown-item osint-col-toggle"
|
||||||
|
data-col-index="{{ forloop.counter0 }}"
|
||||||
|
href="#"
|
||||||
|
onclick="return false;">
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
|
||||||
|
<span>{{ column.label }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="button is-small is-light" type="button" disabled>
|
||||||
|
<span class="icon is-small"><i class="fa-solid fa-database"></i></span>
|
||||||
|
<span>Static</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="osint-results-count">
|
||||||
|
fetched {{ osint_result_count }} result{% if osint_result_count != 1 %}s{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-container osint-results-table-wrap">
|
<div class="table-container osint-results-table-wrap">
|
||||||
<table class="table is-fullwidth is-hoverable osint-results-table">
|
<table class="table is-fullwidth is-hoverable osint-results-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in osint_columns %}
|
{% for column in osint_columns %}
|
||||||
<th>
|
<th data-osint-col="{{ forloop.counter0 }}" class="osint-col-{{ forloop.counter0 }}">
|
||||||
{% if column.sortable %}
|
{% if column.sortable %}
|
||||||
<a
|
<a
|
||||||
class="osint-sort-link"
|
class="osint-sort-link"
|
||||||
@@ -87,7 +121,7 @@
|
|||||||
{% for row in osint_rows %}
|
{% for row in osint_rows %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for cell in row.cells %}
|
{% for cell in row.cells %}
|
||||||
<td>
|
<td data-osint-col="{{ forloop.counter0 }}" class="osint-col-{{ forloop.counter0 }}">
|
||||||
{% if cell.kind == "id_copy" %}
|
{% if cell.kind == "id_copy" %}
|
||||||
<a
|
<a
|
||||||
class="button is-small has-text-grey"
|
class="button is-small has-text-grey"
|
||||||
@@ -219,7 +253,95 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="help has-text-grey">
|
<style>
|
||||||
{{ osint_result_count }} result{% if osint_result_count != 1 %}s{% endif %}
|
#{{ osint_table_id }} .osint-results-meta {
|
||||||
</p>
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
#{{ osint_table_id }} .osint-results-meta-left {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.42rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#{{ osint_table_id }} .osint-results-count {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
#{{ osint_table_id }} .osint-col-toggle .icon {
|
||||||
|
color: #3273dc;
|
||||||
|
}
|
||||||
|
#{{ osint_table_id }} .osint-col-toggle.is-hidden-col .icon {
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const tableId = "{{ osint_table_id|escapejs }}";
|
||||||
|
const shell = document.getElementById(tableId);
|
||||||
|
if (!shell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storageKey = "gia_osint_hidden_cols_v1:" + tableId;
|
||||||
|
let hidden = [];
|
||||||
|
try {
|
||||||
|
hidden = JSON.parse(localStorage.getItem(storageKey) || "[]");
|
||||||
|
} catch (e) {
|
||||||
|
hidden = [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(hidden)) {
|
||||||
|
hidden = [];
|
||||||
|
}
|
||||||
|
hidden = hidden.map(String);
|
||||||
|
|
||||||
|
const applyVisibility = function () {
|
||||||
|
const hiddenSet = new Set(hidden);
|
||||||
|
shell.querySelectorAll("[data-osint-col]").forEach(function (node) {
|
||||||
|
const idx = String(node.getAttribute("data-osint-col") || "");
|
||||||
|
node.style.display = hiddenSet.has(idx) ? "none" : "";
|
||||||
|
});
|
||||||
|
shell.querySelectorAll(".osint-col-toggle").forEach(function (toggle) {
|
||||||
|
const idx = String(toggle.getAttribute("data-col-index") || "");
|
||||||
|
const isHidden = hiddenSet.has(idx);
|
||||||
|
toggle.classList.toggle("is-hidden-col", isHidden);
|
||||||
|
const icon = toggle.querySelector("i");
|
||||||
|
if (icon) {
|
||||||
|
icon.className = isHidden ? "fa-solid fa-xmark" : "fa-solid fa-check";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const persist = function () {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(hidden));
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore storage failures.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
shell.querySelectorAll(".osint-col-toggle").forEach(function (toggle) {
|
||||||
|
toggle.addEventListener("click", function () {
|
||||||
|
const idx = String(toggle.getAttribute("data-col-index") || "");
|
||||||
|
if (!idx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hidden.includes(idx)) {
|
||||||
|
hidden = hidden.filter(function (item) { return item !== idx; });
|
||||||
|
} else {
|
||||||
|
hidden.push(idx);
|
||||||
|
}
|
||||||
|
persist();
|
||||||
|
applyVisibility();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
applyVisibility();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from core.models import (
|
|||||||
WorkspaceConversation,
|
WorkspaceConversation,
|
||||||
)
|
)
|
||||||
from core.realtime.typing_state import get_person_typing_state
|
from core.realtime.typing_state import get_person_typing_state
|
||||||
from core.views.workspace import _build_engage_payload, _parse_draft_options
|
from core.views.workspace import INSIGHT_METRICS, _build_engage_payload, _parse_draft_options
|
||||||
|
|
||||||
COMPOSE_WS_TOKEN_SALT = "compose-ws"
|
COMPOSE_WS_TOKEN_SALT = "compose-ws"
|
||||||
COMPOSE_ENGAGE_TOKEN_SALT = "compose-engage"
|
COMPOSE_ENGAGE_TOKEN_SALT = "compose-engage"
|
||||||
@@ -169,6 +169,320 @@ def _serialize_message(msg: Message) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
THREAD_METRIC_FRAGMENT_SPECS = (
|
||||||
|
{
|
||||||
|
"slug": "stability_score",
|
||||||
|
"title": "Stability Score",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "stability_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "stability_confidence",
|
||||||
|
"title": "Stability Confidence",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "stability_confidence",
|
||||||
|
"precision": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sample_messages",
|
||||||
|
"title": "Sample Messages",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "stability_sample_messages",
|
||||||
|
"precision": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sample_days",
|
||||||
|
"title": "Sample Days",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "stability_sample_days",
|
||||||
|
"precision": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "commitment_inbound",
|
||||||
|
"title": "Commit In",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "commitment_inbound_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "commitment_outbound",
|
||||||
|
"title": "Commit Out",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "commitment_outbound_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "commitment_confidence",
|
||||||
|
"title": "Commit Confidence",
|
||||||
|
"source": "conversation",
|
||||||
|
"field": "commitment_confidence",
|
||||||
|
"precision": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "inbound_messages",
|
||||||
|
"title": "Inbound Messages",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "inbound_messages",
|
||||||
|
"precision": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "outbound_messages",
|
||||||
|
"title": "Outbound Messages",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "outbound_messages",
|
||||||
|
"precision": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "reciprocity_score",
|
||||||
|
"title": "Reciprocity",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "reciprocity_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "continuity_score",
|
||||||
|
"title": "Continuity",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "continuity_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "response_score",
|
||||||
|
"title": "Response",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "response_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "volatility_score",
|
||||||
|
"title": "Volatility",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "volatility_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "inbound_response_score",
|
||||||
|
"title": "Inbound Response",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "inbound_response_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "outbound_response_score",
|
||||||
|
"title": "Outbound Response",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "outbound_response_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "balance_inbound_score",
|
||||||
|
"title": "Inbound Balance",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "balance_inbound_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "balance_outbound_score",
|
||||||
|
"title": "Outbound Balance",
|
||||||
|
"source": "snapshot",
|
||||||
|
"field": "balance_outbound_score",
|
||||||
|
"precision": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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 _workspace_conversation_for_person(user, person):
|
||||||
|
if person is None:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
WorkspaceConversation.objects.filter(
|
||||||
|
user=user,
|
||||||
|
participants=person,
|
||||||
|
)
|
||||||
|
.order_by("-last_event_ts", "-created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _counterpart_identifiers_for_person(user, person):
|
||||||
|
if person is None:
|
||||||
|
return set()
|
||||||
|
values = (
|
||||||
|
PersonIdentifier.objects.filter(user=user, person=person)
|
||||||
|
.values_list("identifier", flat=True)
|
||||||
|
)
|
||||||
|
return {str(value or "").strip() for value in values if str(value or "").strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def _message_is_outgoing_for_analysis(msg, counterpart_identifiers):
|
||||||
|
sender = str(getattr(msg, "sender_uuid", "") or "").strip()
|
||||||
|
if sender and sender in counterpart_identifiers:
|
||||||
|
return False
|
||||||
|
return _is_outgoing(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_gap_duration(ms_value):
|
||||||
|
value = max(0, int(ms_value or 0))
|
||||||
|
seconds = value // 1000
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds}s"
|
||||||
|
minutes = seconds // 60
|
||||||
|
if minutes < 60:
|
||||||
|
return f"{minutes}m"
|
||||||
|
hours = minutes // 60
|
||||||
|
rem_minutes = minutes % 60
|
||||||
|
if rem_minutes == 0:
|
||||||
|
return f"{hours}h"
|
||||||
|
return f"{hours}h {rem_minutes}m"
|
||||||
|
|
||||||
|
|
||||||
|
def _score_from_lag_for_thread(lag_ms, target_hours=4):
|
||||||
|
if lag_ms is None:
|
||||||
|
return 50.0
|
||||||
|
target_ms = max(1, target_hours) * 60 * 60 * 1000
|
||||||
|
return max(0.0, min(100.0, 100.0 / (1.0 + (lag_ms / target_ms))))
|
||||||
|
|
||||||
|
|
||||||
|
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 _format_metric_fragment_value(value, precision):
|
||||||
|
if value is None:
|
||||||
|
return "-"
|
||||||
|
try:
|
||||||
|
number = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
if int(precision or 0) <= 0:
|
||||||
|
return str(int(round(number)))
|
||||||
|
rounded = round(number, int(precision))
|
||||||
|
if float(rounded).is_integer():
|
||||||
|
return str(int(rounded))
|
||||||
|
return f"{rounded:.{int(precision)}f}"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_thread_metric_fragments(conversation):
|
||||||
|
if conversation is None:
|
||||||
|
return []
|
||||||
|
snapshot = conversation.metric_snapshots.first()
|
||||||
|
fragments = []
|
||||||
|
for spec in THREAD_METRIC_FRAGMENT_SPECS:
|
||||||
|
if spec["source"] == "snapshot":
|
||||||
|
source_obj = snapshot
|
||||||
|
else:
|
||||||
|
source_obj = conversation
|
||||||
|
if source_obj is None:
|
||||||
|
continue
|
||||||
|
value = getattr(source_obj, spec["field"], None)
|
||||||
|
copy = _metric_copy(spec["slug"], spec["title"])
|
||||||
|
fragments.append(
|
||||||
|
{
|
||||||
|
"slug": spec["slug"],
|
||||||
|
"title": copy["title"],
|
||||||
|
"value": _format_metric_fragment_value(value, spec.get("precision", 2)),
|
||||||
|
"calculation": copy["calculation"],
|
||||||
|
"psychology": copy["psychology"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return fragments
|
||||||
|
|
||||||
|
|
||||||
|
def _build_gap_fragment(is_outgoing_reply, lag_ms, snapshot):
|
||||||
|
metric_slug = "outbound_response_score" if is_outgoing_reply else "inbound_response_score"
|
||||||
|
copy = _metric_copy(metric_slug, "Response Score")
|
||||||
|
score_value = None
|
||||||
|
if snapshot is not None:
|
||||||
|
score_value = getattr(
|
||||||
|
snapshot,
|
||||||
|
"outbound_response_score" if is_outgoing_reply else "inbound_response_score",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if score_value is None:
|
||||||
|
score_value = _score_from_lag_for_thread(lag_ms)
|
||||||
|
return {
|
||||||
|
"title": "Unseen Gap",
|
||||||
|
"focus": "Your reply delay" if is_outgoing_reply else "Counterpart reply delay",
|
||||||
|
"lag": _format_gap_duration(lag_ms),
|
||||||
|
"score": _format_metric_fragment_value(score_value, 2),
|
||||||
|
"calculation": copy["calculation"],
|
||||||
|
"psychology": copy["psychology"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_messages_with_artifacts(
|
||||||
|
messages,
|
||||||
|
counterpart_identifiers=None,
|
||||||
|
conversation=None,
|
||||||
|
seed_previous=None,
|
||||||
|
):
|
||||||
|
rows = list(messages or [])
|
||||||
|
serialized = [_serialize_message(msg) for msg in rows]
|
||||||
|
for item in serialized:
|
||||||
|
item["gap_fragments"] = []
|
||||||
|
item["metric_fragments"] = []
|
||||||
|
|
||||||
|
counterpart_identifiers = set(counterpart_identifiers or [])
|
||||||
|
snapshot = conversation.metric_snapshots.first() if conversation is not None else None
|
||||||
|
|
||||||
|
prev_msg = seed_previous
|
||||||
|
prev_ts = int(prev_msg.ts or 0) if prev_msg is not None else None
|
||||||
|
prev_outgoing = (
|
||||||
|
_message_is_outgoing_for_analysis(prev_msg, counterpart_identifiers)
|
||||||
|
if prev_msg is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, msg in enumerate(rows):
|
||||||
|
current_ts = int(msg.ts or 0)
|
||||||
|
current_outgoing = _message_is_outgoing_for_analysis(msg, counterpart_identifiers)
|
||||||
|
if (
|
||||||
|
prev_msg is not None
|
||||||
|
and prev_ts is not None
|
||||||
|
and prev_outgoing is not None
|
||||||
|
and current_outgoing != prev_outgoing
|
||||||
|
and current_ts >= prev_ts
|
||||||
|
):
|
||||||
|
lag_ms = current_ts - prev_ts
|
||||||
|
serialized[idx]["gap_fragments"].append(
|
||||||
|
_build_gap_fragment(current_outgoing, lag_ms, snapshot)
|
||||||
|
)
|
||||||
|
prev_msg = msg
|
||||||
|
prev_ts = current_ts
|
||||||
|
prev_outgoing = current_outgoing
|
||||||
|
|
||||||
|
if serialized:
|
||||||
|
serialized[-1]["metric_fragments"] = _build_thread_metric_fragments(conversation)
|
||||||
|
|
||||||
|
return serialized
|
||||||
|
|
||||||
|
|
||||||
def _owner_name(user) -> str:
|
def _owner_name(user) -> str:
|
||||||
return (
|
return (
|
||||||
user.first_name
|
user.first_name
|
||||||
@@ -571,6 +885,21 @@ def _quick_insights_rows(conversation):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def _build_engage_prompt(owner_name, person_name, transcript):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -743,6 +1072,10 @@ def _panel_context(
|
|||||||
base = _context_base(request.user, service, identifier, person)
|
base = _context_base(request.user, service, identifier, person)
|
||||||
limit = _safe_limit(request.GET.get("limit") or request.POST.get("limit"))
|
limit = _safe_limit(request.GET.get("limit") or request.POST.get("limit"))
|
||||||
session_bundle = _load_messages(request.user, base["person_identifier"], limit)
|
session_bundle = _load_messages(request.user, base["person_identifier"], limit)
|
||||||
|
conversation = _workspace_conversation_for_person(request.user, base["person"])
|
||||||
|
counterpart_identifiers = _counterpart_identifiers_for_person(
|
||||||
|
request.user, base["person"]
|
||||||
|
)
|
||||||
last_ts = 0
|
last_ts = 0
|
||||||
if session_bundle["messages"]:
|
if session_bundle["messages"]:
|
||||||
last_ts = int(session_bundle["messages"][-1].ts or 0)
|
last_ts = int(session_bundle["messages"][-1].ts or 0)
|
||||||
@@ -773,9 +1106,11 @@ def _panel_context(
|
|||||||
"person_identifier": base["person_identifier"],
|
"person_identifier": base["person_identifier"],
|
||||||
"session": session_bundle["session"],
|
"session": session_bundle["session"],
|
||||||
"messages": session_bundle["messages"],
|
"messages": session_bundle["messages"],
|
||||||
"serialized_messages": [
|
"serialized_messages": _serialize_messages_with_artifacts(
|
||||||
_serialize_message(msg) for msg in session_bundle["messages"]
|
session_bundle["messages"],
|
||||||
],
|
counterpart_identifiers=counterpart_identifiers,
|
||||||
|
conversation=conversation,
|
||||||
|
),
|
||||||
"last_ts": last_ts,
|
"last_ts": last_ts,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"notice_message": notice,
|
"notice_message": notice,
|
||||||
@@ -900,13 +1235,18 @@ class ComposeThread(LoginRequiredMixin, View):
|
|||||||
base = _context_base(request.user, service, identifier, person)
|
base = _context_base(request.user, service, identifier, person)
|
||||||
latest_ts = after_ts
|
latest_ts = after_ts
|
||||||
messages = []
|
messages = []
|
||||||
|
seed_previous = None
|
||||||
if base["person_identifier"] is not None:
|
if base["person_identifier"] is not None:
|
||||||
session, _ = ChatSession.objects.get_or_create(
|
session, _ = ChatSession.objects.get_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
identifier=base["person_identifier"],
|
identifier=base["person_identifier"],
|
||||||
)
|
)
|
||||||
queryset = Message.objects.filter(user=request.user, session=session)
|
base_queryset = Message.objects.filter(user=request.user, session=session)
|
||||||
|
queryset = base_queryset
|
||||||
if after_ts > 0:
|
if after_ts > 0:
|
||||||
|
seed_previous = (
|
||||||
|
base_queryset.filter(ts__lte=after_ts).order_by("-ts").first()
|
||||||
|
)
|
||||||
queryset = queryset.filter(ts__gt=after_ts)
|
queryset = queryset.filter(ts__gt=after_ts)
|
||||||
messages = list(
|
messages = list(
|
||||||
queryset.select_related(
|
queryset.select_related(
|
||||||
@@ -924,8 +1264,17 @@ class ComposeThread(LoginRequiredMixin, View):
|
|||||||
)
|
)
|
||||||
if newest:
|
if newest:
|
||||||
latest_ts = max(latest_ts, int(newest))
|
latest_ts = max(latest_ts, int(newest))
|
||||||
|
conversation = _workspace_conversation_for_person(request.user, base["person"])
|
||||||
|
counterpart_identifiers = _counterpart_identifiers_for_person(
|
||||||
|
request.user, base["person"]
|
||||||
|
)
|
||||||
payload = {
|
payload = {
|
||||||
"messages": [_serialize_message(msg) for msg in messages],
|
"messages": _serialize_messages_with_artifacts(
|
||||||
|
messages,
|
||||||
|
counterpart_identifiers=counterpart_identifiers,
|
||||||
|
conversation=conversation,
|
||||||
|
seed_previous=seed_previous,
|
||||||
|
),
|
||||||
"last_ts": latest_ts,
|
"last_ts": latest_ts,
|
||||||
"typing": get_person_typing_state(
|
"typing": get_person_typing_state(
|
||||||
user_id=request.user.id,
|
user_id=request.user.id,
|
||||||
@@ -1122,6 +1471,7 @@ class ComposeQuickInsights(LoginRequiredMixin, View):
|
|||||||
)
|
)
|
||||||
|
|
||||||
payload = _quick_insights_rows(conversation)
|
payload = _quick_insights_rows(conversation)
|
||||||
|
participant_state = _participant_feedback_state_label(conversation, person)
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -1129,7 +1479,9 @@ class ComposeQuickInsights(LoginRequiredMixin, View):
|
|||||||
"summary": {
|
"summary": {
|
||||||
"person_name": person.name,
|
"person_name": person.name,
|
||||||
"platform": conversation.get_platform_type_display(),
|
"platform": conversation.get_platform_type_display(),
|
||||||
"state": conversation.get_stability_state_display(),
|
"state": participant_state
|
||||||
|
or conversation.get_stability_state_display(),
|
||||||
|
"stability_state": conversation.get_stability_state_display(),
|
||||||
"thread": conversation.platform_thread_id or "",
|
"thread": conversation.platform_thread_id or "",
|
||||||
"last_event": _format_ts_label(conversation.last_event_ts or 0)
|
"last_event": _format_ts_label(conversation.last_event_ts or 0)
|
||||||
if conversation.last_event_ts
|
if conversation.last_event_ts
|
||||||
@@ -1150,6 +1502,7 @@ class ComposeQuickInsights(LoginRequiredMixin, View):
|
|||||||
"docs": [
|
"docs": [
|
||||||
"Each row shows current value, percent change vs previous point, and data-point count.",
|
"Each row shows current value, percent change vs previous point, and data-point count.",
|
||||||
"Arrow color indicates improving or risk direction for that metric.",
|
"Arrow color indicates improving or risk direction for that metric.",
|
||||||
|
"State uses participant feedback (Withdrawing/Overextending/Balanced) when available.",
|
||||||
"Face indicator maps value range to positive, mixed, or strained climate.",
|
"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.",
|
"Use this card for fast triage; open AI Workspace for full graphs and details.",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ class OSINTListBase(ObjectList):
|
|||||||
object_list: list[Any],
|
object_list: list[Any],
|
||||||
request_type: str,
|
request_type: str,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
|
context_type = _context_type(request_type)
|
||||||
rows = []
|
rows = []
|
||||||
for item in object_list:
|
for item in object_list:
|
||||||
row = {"id": str(item.pk), "cells": [], "actions": []}
|
row = {"id": str(item.pk), "cells": [], "actions": []}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class PersonaList(LoginRequiredMixin, OSINTListBase):
|
|||||||
class PersonaCreate(LoginRequiredMixin, ObjectCreate):
|
class PersonaCreate(LoginRequiredMixin, ObjectCreate):
|
||||||
model = Persona
|
model = Persona
|
||||||
form_class = PersonaForm
|
form_class = PersonaForm
|
||||||
|
window_content = "mixins/window-content/persona-form.html"
|
||||||
|
|
||||||
submit_url_name = "persona_create"
|
submit_url_name = "persona_create"
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class PersonaCreate(LoginRequiredMixin, ObjectCreate):
|
|||||||
class PersonaUpdate(LoginRequiredMixin, ObjectUpdate):
|
class PersonaUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
model = Persona
|
model = Persona
|
||||||
form_class = PersonaForm
|
form_class = PersonaForm
|
||||||
|
window_content = "mixins/window-content/persona-form.html"
|
||||||
|
|
||||||
submit_url_name = "persona_update"
|
submit_url_name = "persona_update"
|
||||||
|
|
||||||
|
|||||||
@@ -629,6 +629,56 @@ def _compose_page_url_for_person(user, person):
|
|||||||
return f"{reverse('compose_page')}?{query}"
|
return f"{reverse('compose_page')}?{query}"
|
||||||
|
|
||||||
|
|
||||||
|
def _participant_feedback_display(conversation, person):
|
||||||
|
payload = conversation.participant_feedback or {}
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw = payload.get(str(person.id)) or {}
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
state_key = str(raw.get("state") or "").strip().lower()
|
||||||
|
state_label = {
|
||||||
|
"withdrawing": "Withdrawing",
|
||||||
|
"overextending": "Overextending",
|
||||||
|
"balanced": "Balanced",
|
||||||
|
}.get(state_key, "Unknown")
|
||||||
|
state_icon = {
|
||||||
|
"withdrawing": "fa-regular fa-face-frown",
|
||||||
|
"overextending": "fa-regular fa-face-meh",
|
||||||
|
"balanced": "fa-regular fa-face-smile",
|
||||||
|
}.get(state_key, "fa-regular fa-face-meh-blank")
|
||||||
|
state_class = {
|
||||||
|
"withdrawing": "has-text-danger",
|
||||||
|
"overextending": "has-text-warning",
|
||||||
|
"balanced": "has-text-success",
|
||||||
|
}.get(state_key, "has-text-grey")
|
||||||
|
|
||||||
|
updated_label = ""
|
||||||
|
updated_raw = raw.get("updated_at")
|
||||||
|
if updated_raw:
|
||||||
|
try:
|
||||||
|
dt_value = datetime.fromisoformat(str(updated_raw))
|
||||||
|
if dt_value.tzinfo is None:
|
||||||
|
dt_value = dt_value.replace(tzinfo=timezone.utc)
|
||||||
|
updated_label = dj_timezone.localtime(dt_value).strftime("%Y-%m-%d %H:%M")
|
||||||
|
except Exception:
|
||||||
|
updated_label = str(updated_raw)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"state_key": state_key or "unknown",
|
||||||
|
"state_label": state_label,
|
||||||
|
"state_icon": state_icon,
|
||||||
|
"state_class": state_class,
|
||||||
|
"inbound_messages": raw.get("inbound_messages"),
|
||||||
|
"outbound_messages": raw.get("outbound_messages"),
|
||||||
|
"sample_messages": raw.get("sample_messages"),
|
||||||
|
"sample_days": raw.get("sample_days"),
|
||||||
|
"updated_at_label": updated_label,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _message_rows_for_person(user, person, limit):
|
def _message_rows_for_person(user, person, limit):
|
||||||
sessions = ChatSession.objects.filter(user=user, identifier__person=person)
|
sessions = ChatSession.objects.filter(user=user, identifier__person=person)
|
||||||
identifiers = set(
|
identifiers = set(
|
||||||
@@ -3302,6 +3352,9 @@ class AIWorkspacePersonWidget(LoginRequiredMixin, View):
|
|||||||
"widget_options": 'gs-w="8" gs-h="11" gs-x="4" gs-y="0" gs-min-w="4"',
|
"widget_options": 'gs-w="8" gs-h="11" gs-x="4" gs-y="0" gs-min-w="4"',
|
||||||
"person": person,
|
"person": person,
|
||||||
"workspace_conversation": conversation,
|
"workspace_conversation": conversation,
|
||||||
|
"participant_feedback_display": _participant_feedback_display(
|
||||||
|
conversation, person
|
||||||
|
),
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"ai_operations": [
|
"ai_operations": [
|
||||||
("artifacts", "Plan"),
|
("artifacts", "Plan"),
|
||||||
@@ -3454,6 +3507,10 @@ class AIWorkspaceInformation(LoginRequiredMixin, View):
|
|||||||
"ai_workspace_insight_graphs",
|
"ai_workspace_insight_graphs",
|
||||||
kwargs={"type": "page", "person_id": person.id},
|
kwargs={"type": "page", "person_id": person.id},
|
||||||
),
|
),
|
||||||
|
"information_url": reverse(
|
||||||
|
"ai_workspace_information",
|
||||||
|
kwargs={"type": "page", "person_id": person.id},
|
||||||
|
),
|
||||||
"help_url": reverse(
|
"help_url": reverse(
|
||||||
"ai_workspace_insight_help",
|
"ai_workspace_insight_help",
|
||||||
kwargs={"type": "page", "person_id": person.id},
|
kwargs={"type": "page", "person_id": person.id},
|
||||||
@@ -3734,9 +3791,7 @@ class AIWorkspaceRunOperation(LoginRequiredMixin, View):
|
|||||||
limit = max(5, min(limit, 200))
|
limit = max(5, min(limit, 200))
|
||||||
user_notes = request.GET.get("user_notes", "")
|
user_notes = request.GET.get("user_notes", "")
|
||||||
|
|
||||||
messages = AIWorkspacePersonWidget()._recent_messages(
|
messages = _recent_messages_for_person(request.user, person, limit)
|
||||||
request.user, person, limit
|
|
||||||
)
|
|
||||||
owner_name = (
|
owner_name = (
|
||||||
request.user.first_name
|
request.user.first_name
|
||||||
or request.user.get_full_name().strip()
|
or request.user.get_full_name().strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user