Continue AI features and improve protocol support

This commit is contained in:
2026-02-15 16:57:32 +00:00
parent 2d3b8fdac6
commit 85e97e895d
62 changed files with 5472 additions and 441 deletions

View File

@@ -295,7 +295,10 @@
<a class="navbar-item" href="{% url 'signal' %}">
Signal
</a>
<a class="navbar-item" href="#">
<a class="navbar-item" href="{% url 'whatsapp' %}">
WhatsApp
</a>
<a class="navbar-item" href="{% url 'instagram' %}">
Instagram
</a>
</div>
@@ -308,6 +311,20 @@
<div class="navbar-end">
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a
class="navbar-link"
hx-get="{% url 'compose_contacts_dropdown' %}"
hx-target="#nav-compose-contacts"
hx-trigger="click once"
hx-swap="innerHTML">
<span class="icon is-small"><i class="fa-solid fa-paper-plane"></i></span>
<span style="margin-left: 0.35rem;">Message</span>
</a>
<div class="navbar-dropdown" id="nav-compose-contacts">
<a class="navbar-item is-disabled">Load contacts</a>
</div>
</div>
<a class="navbar-item" href="{% url 'ai_workspace' %}">
AI
</a>

View File

@@ -0,0 +1,110 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="columns is-multiline">
<div class="column is-12">
<nav class="breadcrumb is-small" aria-label="breadcrumbs">
<ul>
<li><a href="{{ workspace_url }}">AI Workspace</a></li>
<li><a href="{{ graphs_url }}">Insight Graphs</a></li>
<li class="is-active"><a aria-current="page">{{ metric.title }}</a></li>
</ul>
</nav>
</div>
<div class="column is-12">
<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>
</div>
<div class="column is-5">
<div class="box">
<p class="heading">{{ metric_group.title }}</p>
<p class="title is-5" style="margin-bottom: 0.5rem;">{{ metric.title }}</p>
<p><strong>Current Value:</strong> {{ metric_value|default:"-" }}</p>
<p style="margin-top: 0.65rem;"><strong>How It Is Calculated</strong></p>
<p>{{ metric.calculation }}</p>
<p style="margin-top: 0.65rem;"><strong>Psychological Interpretation</strong></p>
<p>{{ metric.psychology }}</p>
{% if metric_psychology_hint %}
<article class="message is-info is-light" style="margin-top: 0.75rem;">
<div class="message-body">
{{ metric_psychology_hint }}
</div>
</article>
{% endif %}
</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="{{ help_url }}">
<span class="icon is-small"><i class="fa-solid fa-circle-question"></i></span>
<span>Scoring Help</span>
</a>
</div>
</div>
<div class="column is-7">
<div class="box">
<p class="heading">History</p>
<div style="height: 360px;">
<canvas id="metric-detail-chart"></canvas>
</div>
{% if not graph_points %}
<p class="is-size-7 has-text-grey" style="margin-top: 0.65rem;">
No historical points yet for this metric.
</p>
{% endif %}
</div>
</div>
</div>
{{ graph_points|json_script:"metric-detail-points" }}
<script src="{% static 'js/chart.js' %}"></script>
<script>
(function() {
var node = document.getElementById("metric-detail-points");
if (!node) {
return;
}
var points = JSON.parse(node.textContent || "[]");
var labels = points.map(function(row) {
var dt = new Date(row.x);
return dt.toLocaleString();
});
var values = points.map(function(row) {
return row.y;
});
var ctx = document.getElementById("metric-detail-chart");
if (!ctx) {
return;
}
new Chart(ctx.getContext("2d"), {
type: "line",
data: {
labels: labels,
datasets: [
{
label: "{{ metric.title|escapejs }}",
data: values,
borderColor: "#3273dc",
backgroundColor: "rgba(50,115,220,0.12)",
borderWidth: 2,
pointRadius: 2,
tension: 0.28,
spanGaps: true
}
]
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: true
}
}
}
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,115 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="columns is-multiline">
<div class="column is-12">
<nav class="breadcrumb is-small" aria-label="breadcrumbs">
<ul>
<li><a href="{{ workspace_url }}">AI Workspace</a></li>
<li class="is-active"><a aria-current="page">Insight Graphs</a></li>
</ul>
</nav>
</div>
<div class="column is-12">
<h1 class="title is-4" style="margin-bottom: 0.35rem;">Insight Graphs: {{ person.name }}</h1>
<p class="is-size-7 has-text-grey">
Historical metrics for workspace {{ workspace_conversation.id }}
</p>
<div class="buttons are-small" style="margin-top: 0.6rem;">
<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>
{% for graph in graph_cards %}
<div class="column is-6">
<div class="box">
<div class="is-flex is-justify-content-space-between is-align-items-center" style="margin-bottom: 0.45rem;">
<div>
<p class="heading">{{ graph.group_title }}</p>
<p class="title is-6" style="margin-bottom: 0.2rem;">{{ graph.title }}</p>
<p class="is-size-7 has-text-grey">{{ graph.count }} point{{ graph.count|pluralize }}</p>
</div>
<div class="buttons are-small" style="margin: 0;">
<a class="button is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric=graph.slug %}">
Detail
</a>
<a class="button is-light" href="{{ help_url }}#group-{{ graph.group }}">
How It Works
</a>
</div>
</div>
<div style="height: 250px;">
<canvas id="graph-{{ graph.slug }}"></canvas>
</div>
</div>
</div>
{% endfor %}
</div>
{{ graph_cards|json_script:"insight-graph-cards" }}
<script src="{% static 'js/chart.js' %}"></script>
<script>
(function() {
var node = document.getElementById("insight-graph-cards");
if (!node) {
return;
}
var graphs = JSON.parse(node.textContent || "[]");
var palette = ["#3273dc", "#23d160", "#ffdd57", "#ff3860", "#7957d5", "#00d1b2"];
graphs.forEach(function(graph, idx) {
var canvas = document.getElementById("graph-" + graph.slug);
if (!canvas) {
return;
}
var labels = (graph.points || []).map(function(row) {
return new Date(row.x).toLocaleString();
});
var values = (graph.points || []).map(function(row) {
return row.y;
});
var color = palette[idx % palette.length];
var yScale = {};
if (graph.y_min !== null && graph.y_min !== undefined) {
yScale.min = graph.y_min;
}
if (graph.y_max !== null && graph.y_max !== undefined) {
yScale.max = graph.y_max;
}
new Chart(canvas.getContext("2d"), {
type: "line",
data: {
labels: labels,
datasets: [
{
label: graph.title,
data: values,
borderColor: color,
backgroundColor: color + "33",
borderWidth: 2,
pointRadius: 2,
tension: 0.24,
spanGaps: true
}
]
},
options: {
maintainAspectRatio: false,
scales: {
y: yScale
},
plugins: {
legend: {
display: false
}
}
}
});
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="columns is-multiline">
<div class="column is-12">
<nav class="breadcrumb is-small" aria-label="breadcrumbs">
<ul>
<li><a href="{{ workspace_url }}">AI Workspace</a></li>
<li><a href="{{ graphs_url }}">Insight Graphs</a></li>
<li class="is-active"><a aria-current="page">Scoring Help</a></li>
</ul>
</nav>
</div>
<div class="column is-12">
<h1 class="title is-4" style="margin-bottom: 0.35rem;">Scoring Help: {{ person.name }}</h1>
<p class="is-size-7 has-text-grey">
Combined explanation for each metric collection group and what it can
imply in relationship dynamics.
</p>
</div>
{% for group_key, group in groups.items %}
<div class="column is-12" id="group-{{ group_key }}">
<div class="box">
<p class="heading">{{ group.title }}</p>
<p style="margin-bottom: 0.75rem;">{{ group.summary }}</p>
{% for metric in metrics %}
{% if metric.group == group_key %}
<article class="message is-light" style="margin-bottom: 0.6rem;">
<div class="message-body">
<p><strong>{{ metric.title }}</strong>: {{ metric.value|default:"-" }}</p>
<p><strong>Calculation:</strong> {{ metric.calculation }}</p>
<p><strong>Psychological Read:</strong> {{ metric.psychology }}</p>
</div>
</article>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -8,4 +8,13 @@
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
{% if selected_person_id %}
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'ai_workspace_person' type='widget' person_id=selected_person_id %}"
hx-target="#widgets-here"
hx-trigger="load delay:250ms"
hx-swap="afterend"
style="display: none;"></div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
<div class="columns is-centered">
<div class="column is-10-tablet is-9-desktop is-8-widescreen">
<div id="compose-page-panel">
{% include "partials/compose-panel.html" %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,9 +3,9 @@
{% block load_widgets %}
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_accounts' type='widget' %}"
hx-get="{% url accounts_url_name type='widget' %}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
{% endblock %}
{% endblock %}

View File

@@ -13,6 +13,37 @@
{{ result_text }}
</div>
{% else %}
{% if ai_request_status %}
<div class="tags" style="margin-bottom: 0.45rem;">
<span class="tag is-light">Request {{ ai_request_status|title }}</span>
<span class="tag is-light">Messages {{ ai_request_message_count|default:0 }}</span>
{% if ai_result_created_at %}
<span class="tag is-light">Result {{ ai_result_created_at }}</span>
{% endif %}
{% if ai_request_started_at %}
<span class="tag is-light">Started {{ ai_request_started_at }}</span>
{% endif %}
{% if ai_request_finished_at %}
<span class="tag is-light">Finished {{ ai_request_finished_at }}</span>
{% endif %}
</div>
{% if ai_request_window_spec %}
<div class="tags" style="margin-bottom: 0.45rem;">
{% if ai_request_window_tags %}
{% for item in ai_request_window_tags %}
<span class="tag is-light">{{ item }}</span>
{% endfor %}
{% else %}
<span class="tag is-light">Window selected</span>
{% endif %}
{% if ai_request_policy_tags %}
{% for item in ai_request_policy_tags %}
<span class="tag is-light">{{ item }}</span>
{% endfor %}
{% endif %}
</div>
{% endif %}
{% endif %}
{% if operation == "artifacts" %}
{% if latest_plan %}
{% include "partials/ai-workspace-mitigation-panel.html" with person=person plan=latest_plan rules=latest_plan_rules games=latest_plan_games corrections=latest_plan_corrections fundamentals_text=latest_plan.fundamental_items|join:"\n" mitigation_messages=latest_plan_messages latest_export=latest_plan_export notice_message=mitigation_notice_message notice_level=mitigation_notice_level auto_settings=latest_auto_settings active_tab="plan_board" %}
@@ -145,6 +176,72 @@
{% endif %}
{% endif %}
{% if interaction_signals %}
<article class="box" style="padding: 0.55rem; margin-top: 0.55rem; border: 1px solid rgba(0, 0, 0, 0.14); box-shadow: none;">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Interaction Signals</p>
<div class="tags">
{% for signal in interaction_signals %}
<span class="tag is-light">{{ signal.label }} ({{ signal.valence }})</span>
{% endfor %}
</div>
</article>
{% endif %}
{% 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;">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Memory Proposals</p>
{% if memory_proposal_groups %}
<div class="columns is-multiline" style="margin: 0 -0.25rem;">
{% for group in memory_proposal_groups %}
<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>
<ul style="margin: 0 0 0.25rem 1.1rem;">
{% for proposal in group.items %}
<li class="is-size-7" style="margin-bottom: 0.22rem; white-space: pre-wrap;">
{{ proposal.content }}
</li>
{% endfor %}
</ul>
</article>
</div>
{% endfor %}
</div>
{% else %}
{% for proposal in memory_proposals %}
<p class="is-size-7" style="margin-bottom: 0.3rem; white-space: pre-wrap;">
<strong>{{ proposal.kind|title }}</strong>: {{ proposal.content }}
</p>
{% endfor %}
{% endif %}
</article>
{% endif %}
{% if citations %}
<article class="box" style="padding: 0.55rem; margin-top: 0.55rem; border: 1px solid rgba(0, 0, 0, 0.14); box-shadow: none;">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Citations</p>
{% if citation_rows %}
<div class="content is-small" style="margin-bottom: 0;">
{% for row in citation_rows %}
<p class="is-size-7" style="margin-bottom: 0.3rem;">
<span class="tag is-light">{{ row.source_system|default:"event" }}</span>
{% if row.ts_label %}
<span class="has-text-grey">{{ row.ts_label }}</span>
{% endif %}
{% if row.text %}
<span> {{ row.text|truncatechars:140 }}</span>
{% else %}
<code>{{ row.id }}</code>
{% endif %}
</p>
{% endfor %}
</div>
{% else %}
<p class="is-size-7" style="margin: 0;">{{ citations|join:", " }}</p>
{% endif %}
</article>
{% endif %}
{% if operation == "extract_patterns" %}
<article class="box" style="padding: 0.7rem; margin-top: 0.65rem; border: 1px solid rgba(0, 0, 0, 0.14); box-shadow: none;">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.4rem;">Create Framework / Rules / Games</p>

View File

@@ -7,7 +7,54 @@
<p class="is-size-7">{{ plan.objective }}</p>
{% endif %}
</div>
<span class="tag is-light">{{ plan.creation_mode|title }}</span>
<div class="is-flex is-flex-direction-column" style="gap: 0.35rem;">
<span class="tag is-light">{{ plan.creation_mode|title }} / {{ plan.status|title }}</span>
<span class="tag is-light">Created {{ plan.created_at }}</span>
<span class="tag is-light">Updated {{ plan.updated_at }}</span>
{% if plan.source_ai_result_id %}
<span class="tag is-light">Source Result {{ plan.source_ai_result_id }}</span>
{% endif %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_meta_save' type='widget' person_id=person.id plan_id=plan.id %}"
hx-target="#mitigation-shell-{{ person.id }}"
hx-swap="outerHTML">
<input type="hidden" name="active_tab" value="{{ active_tab|default:'plan_board' }}">
<div class="field" style="margin-bottom: 0.3rem;">
<div class="control">
<input class="input is-small" type="text" name="title" value="{{ plan.title }}" placeholder="Plan title">
</div>
</div>
<div class="field" style="margin-bottom: 0.3rem;">
<div class="control">
<textarea class="textarea is-small" rows="2" name="objective" placeholder="Plan objective">{{ plan.objective }}</textarea>
</div>
</div>
<div class="field is-grouped is-grouped-right" style="margin: 0; gap: 0.3rem;">
<div class="control">
<div class="select is-small">
<select name="creation_mode">
{% for value, label in plan_creation_mode_choices %}
<option value="{{ value }}" {% if plan.creation_mode == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<div class="select is-small">
<select name="status">
{% for value, label in plan_status_choices %}
<option value="{{ value }}" {% if plan.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<button type="submit" class="button is-small is-light">Save</button>
</div>
</div>
</form>
</div>
</div>
{% if notice_message %}
@@ -84,6 +131,7 @@
{% for rule in rules %}
<article class="box" style="padding: 0.55rem; margin-bottom: 0.45rem; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;">
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Rule</span>
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Created {{ rule.created_at }}</span>
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_artifact_save' type='widget' person_id=person.id plan_id=plan.id kind='rule' artifact_id=rule.id %}"
@@ -95,7 +143,10 @@
<div class="field" style="margin-bottom: 0.35rem;">
<textarea class="textarea is-small" rows="3" name="body" data-editable="1" readonly>{{ rule.content }}</textarea>
</div>
<input type="hidden" name="enabled" value="1">
<label class="checkbox is-size-7" style="margin-bottom: 0.35rem;">
<input type="checkbox" name="enabled" value="1" {% if rule.enabled %}checked{% endif %}>
Enabled
</label>
<input type="hidden" name="active_tab" value="{{ active_tab|default:'plan_board' }}">
<div class="buttons are-small" style="margin: 0;">
<button type="button" class="button is-link is-light" data-edit-state="view" onclick="giaMitigationToggleEdit(this); return false;">Edit</button>
@@ -136,6 +187,7 @@
{% for game in games %}
<article class="box" style="padding: 0.55rem; margin-bottom: 0.45rem; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;">
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Game</span>
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Created {{ game.created_at }}</span>
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_artifact_save' type='widget' person_id=person.id plan_id=plan.id kind='game' artifact_id=game.id %}"
@@ -147,7 +199,10 @@
<div class="field" style="margin-bottom: 0.35rem;">
<textarea class="textarea is-small" rows="3" name="body" data-editable="1" readonly>{{ game.instructions }}</textarea>
</div>
<input type="hidden" name="enabled" value="1">
<label class="checkbox is-size-7" style="margin-bottom: 0.35rem;">
<input type="checkbox" name="enabled" value="1" {% if game.enabled %}checked{% endif %}>
Enabled
</label>
<input type="hidden" name="active_tab" value="{{ active_tab|default:'plan_board' }}">
<div class="buttons are-small" style="margin: 0;">
<button type="button" class="button is-link is-light" data-edit-state="view" onclick="giaMitigationToggleEdit(this); return false;">Edit</button>
@@ -203,6 +258,7 @@
{% for correction in corrections %}
<article class="box" style="padding: 0.55rem; margin-bottom: 0.5rem; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;">
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Correction</span>
<span class="tag is-light is-small" style="margin-bottom: 0.3rem;">Created {{ correction.created_at }}</span>
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_artifact_save' type='widget' person_id=person.id plan_id=plan.id kind='correction' artifact_id=correction.id %}"
@@ -220,8 +276,41 @@
<label class="label is-small" style="margin-bottom: 0.2rem;">Insight</label>
<textarea class="textarea is-small" rows="2" name="body">{{ correction.clarification }}</textarea>
</div>
<div class="column is-12-mobile is-4-tablet" style="padding: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Perspective</label>
<div class="select is-small is-fullwidth">
<select name="perspective">
{% for value, label in correction.PERSPECTIVE_CHOICES %}
<option value="{{ value }}" {% if correction.perspective == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-12-mobile is-4-tablet" style="padding: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Share Target</label>
<div class="select is-small is-fullwidth">
<select name="share_target">
{% for value, label in correction.SHARE_TARGET_CHOICES %}
<option value="{{ value }}" {% if correction.share_target == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-12-mobile is-4-tablet" style="padding: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Language Style</label>
<div class="select is-small is-fullwidth">
<select name="language_style">
{% for value, label in correction.LANGUAGE_STYLE_CHOICES %}
<option value="{{ value }}" {% if correction.language_style == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<input type="hidden" name="enabled" value="1">
<label class="checkbox is-size-7" style="margin-bottom: 0.35rem;">
<input type="checkbox" name="enabled" value="1" {% if correction.enabled %}checked{% endif %}>
Enabled
</label>
<input type="hidden" name="active_tab" value="{{ active_tab|default:'corrections' }}">
<div class="buttons are-small" style="margin: 0;">
<button class="button is-small is-link is-light">Save Correction</button>
@@ -395,6 +484,12 @@
<p class="is-size-7" style="margin-bottom: 0;">
Last run: {% if auto_settings.last_run_at %}{{ auto_settings.last_run_at }}{% else %}Never{% endif %}
</p>
<p class="is-size-7" style="margin-bottom: 0;">
Created: {{ auto_settings.created_at }} | Updated: {{ auto_settings.updated_at }}
</p>
<p class="is-size-7" style="margin-bottom: 0;">
Last checked event ts: {{ auto_settings.last_checked_event_ts|default:"None" }}
</p>
{% if auto_settings.last_result_summary %}
<p class="is-size-7" style="margin-top: 0.35rem; margin-bottom: 0;">{{ auto_settings.last_result_summary }}</p>
{% endif %}
@@ -463,10 +558,9 @@
<label class="label is-small" style="margin-bottom: 0.25rem;">Bundle</label>
<div class="select is-small">
<select name="artifact_type">
<option value="rulebook">Rulebook</option>
<option value="rules">Rules</option>
<option value="games">Games</option>
<option value="corrections">Corrections</option>
{% for value, label in artifact_type_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
@@ -474,9 +568,9 @@
<label class="label is-small" style="margin-bottom: 0.25rem;">Format</label>
<div class="select is-small">
<select name="export_format">
<option value="markdown">Markdown</option>
<option value="json">JSON</option>
<option value="text">Text</option>
{% for value, label in artifact_format_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
@@ -494,6 +588,11 @@
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.3rem;">
Last Export: {{ latest_export.artifact_type|title }} ({{ latest_export.export_format|upper }})
</p>
<p class="is-size-7" style="margin-bottom: 0.3rem;">
Created {{ latest_export.created_at }} |
Protocol {{ latest_export.protocol_version }} |
Meta {{ latest_export.meta }}
</p>
<pre style="max-height: 14rem; overflow: auto; margin: 0; white-space: pre-wrap; font-size: 0.72rem; line-height: 1.28;">{{ latest_export.payload }}</pre>
</article>
{% endif %}
@@ -505,6 +604,7 @@
{% for message in mitigation_messages %}
<div style="margin-bottom: 0.45rem;">
<span class="tag is-light is-small">{{ message.role }}</span>
<span class="tag is-light is-small">{{ message.created_at }}</span>
<div style="margin-top: 0.15rem; white-space: pre-wrap;">{{ message.text }}</div>
</div>
{% empty %}
@@ -560,9 +660,9 @@
const forceInput = document.getElementById("engage-force-send-" + pid);
const sendBtn = document.getElementById("engage-send-btn-" + pid);
const force =
!!(window.giaWorkspaceState
&& window.giaWorkspaceState[pid]
&& window.giaWorkspaceState[pid].forceSend);
!!(window.giaWorkspaceState
&& window.giaWorkspaceState[pid]
&& window.giaWorkspaceState[pid].forceSend);
if (forceInput) {
forceInput.value = force ? "1" : "0";
}

View File

@@ -10,6 +10,57 @@
<p class="is-size-7 has-text-weight-semibold">Selected Person</p>
<h3 class="title is-5" style="margin-bottom: 0.25rem;">{{ person.name }}</h3>
<p class="is-size-7">Showing last {{ limit }} messages.</p>
<div class="tags" style="margin-top: 0.35rem;">
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='platform' %}">Platform {{ workspace_conversation.platform_type|title }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='thread' %}">Thread {{ workspace_conversation.platform_thread_id|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='workspace_created' %}">Workspace Created {{ workspace_conversation.created_at|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='stability_state' %}">Stability {{ workspace_conversation.stability_state|title }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='stability_score' %}">Stability Score {{ workspace_conversation.stability_score|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='stability_confidence' %}">Confidence {{ workspace_conversation.stability_confidence }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='sample_messages' %}">Sample Msg {{ workspace_conversation.stability_sample_messages }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='sample_days' %}">Sample Days {{ workspace_conversation.stability_sample_days }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='stability_computed' %}">Stability Computed {{ workspace_conversation.stability_last_computed_at|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='commitment_inbound' %}">Commit In {{ workspace_conversation.commitment_inbound_score|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='commitment_outbound' %}">Commit Out {{ workspace_conversation.commitment_outbound_score|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='commitment_confidence' %}">Commit Confidence {{ workspace_conversation.commitment_confidence }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='commitment_computed' %}">Commitment Computed {{ workspace_conversation.commitment_last_computed_at|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='last_event' %}">Last Event {{ workspace_conversation.last_event_ts|default:"-" }}</a>
<a class="tag is-light" href="{% url 'ai_workspace_insight_detail' type='page' person_id=person.id metric='last_ai_run' %}">Last AI Run {{ workspace_conversation.last_ai_run_at|default:"-" }}</a>
</div>
<div class="buttons are-small" style="margin-top: 0.35rem; margin-bottom: 0;">
<a class="button is-light" href="{% url 'ai_workspace_insight_graphs' type='page' person_id=person.id %}">
<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="{% 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>Scoring Help</span>
</a>
</div>
{% with participants=workspace_conversation.participants.all %}
{% if participants %}
<p class="is-size-7" style="margin-top: 0.35rem; margin-bottom: 0;">
Participants:
{% for participant in participants %}
{% if not forloop.first %}, {% endif %}
{{ participant.name }}
{% endfor %}
</p>
{% endif %}
{% endwith %}
{% if workspace_conversation.participant_feedback %}
<p class="is-size-7" style="margin-top: 0.35rem; margin-bottom: 0;">
Participant Feedback: {{ workspace_conversation.participant_feedback }}
</p>
{% endif %}
{% if compose_page_url %}
<div class="buttons are-small" style="margin-top: 0.45rem; margin-bottom: 0;">
<a class="button is-light" href="{{ compose_page_url }}">
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>Manual Text Mode</span>
</a>
</div>
{% endif %}
</div>
<div class="notification is-{{ send_state.level }} is-light" style="padding: 0.5rem 0.75rem;">
@@ -459,32 +510,32 @@
fetch(url, { method: "GET" })
.then(function(resp) { return resp.text(); })
.then(function(html) {
pane.innerHTML = html;
pane.dataset.loaded = "1";
executeInlineScripts(pane);
pane.classList.remove("ai-animate-in");
void pane.offsetWidth;
pane.classList.add("ai-animate-in");
if (cacheAllowed) {
window.giaWorkspaceCache[key] = {
html: html,
ts: Date.now(),
};
persistCache();
setCachedIndicator(true, window.giaWorkspaceCache[key].ts);
} else {
setCachedIndicator(false, null);
}
if (window.htmx) {
window.htmx.process(pane);
}
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
window.giaWorkspaceUseDraft(personId, operation, 0);
}
})
pane.innerHTML = html;
pane.dataset.loaded = "1";
executeInlineScripts(pane);
pane.classList.remove("ai-animate-in");
void pane.offsetWidth;
pane.classList.add("ai-animate-in");
if (cacheAllowed) {
window.giaWorkspaceCache[key] = {
html: html,
ts: Date.now(),
};
persistCache();
setCachedIndicator(true, window.giaWorkspaceCache[key].ts);
} else {
setCachedIndicator(false, null);
}
if (window.htmx) {
window.htmx.process(pane);
}
if (operation === "draft_reply" && typeof window.giaWorkspaceUseDraft === "function") {
window.giaWorkspaceUseDraft(personId, operation, 0);
}
})
.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>';
});
};
window.giaWorkspaceRefresh = function(pid) {
@@ -576,15 +627,15 @@
})
.then(function(resp) { return resp.text(); })
.then(function(html) {
if (statusHost) {
statusHost.innerHTML = html;
}
})
if (statusHost) {
statusHost.innerHTML = html;
}
})
.catch(function() {
if (statusHost) {
statusHost.innerHTML = '<div class="notification is-danger is-light" style="padding: 0.45rem 0.6rem;">Failed to queue draft.</div>';
}
});
if (statusHost) {
statusHost.innerHTML = '<div class="notification is-danger is-light" style="padding: 0.45rem 0.6rem;">Failed to queue draft.</div>';
}
});
};
if (typeof window.giaMitigationShowTab !== "function") {

View File

@@ -0,0 +1,341 @@
<div id="{{ panel_id }}" class="compose-shell">
<div class="is-flex is-justify-content-space-between is-align-items-center" style="gap: 0.5rem; flex-wrap: wrap;">
<div>
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.2rem;">Manual Text Mode</p>
<p class="is-size-6" style="margin-bottom: 0;">
{% if person %}
{{ person.name }}
{% else %}
{{ identifier }}
{% endif %}
</p>
<p class="is-size-7 compose-meta-line" style="margin-bottom: 0;">
{{ service|title }} · {{ identifier }}
</p>
</div>
<div class="buttons are-small" style="margin: 0;">
<a class="button is-light is-rounded" href="{{ ai_workspace_url }}">
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>AI Workspace</span>
</a>
</div>
</div>
<div id="{{ panel_id }}-status" class="compose-status">
{% include "partials/compose-send-status.html" %}
</div>
<div
id="{{ panel_id }}-thread"
class="compose-thread"
data-poll-url="{% url 'compose_thread' %}"
data-service="{{ service }}"
data-identifier="{{ identifier }}"
data-person="{% if person %}{{ person.id }}{% endif %}"
data-limit="{{ limit }}"
data-last-ts="{{ last_ts }}">
{% for msg in serialized_messages %}
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}">
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
<p class="compose-body">{{ msg.text|default:"(no text)" }}</p>
<p class="compose-msg-meta">
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
</p>
</article>
</div>
{% empty %}
<p class="compose-empty">No stored messages for this contact yet.</p>
{% endfor %}
</div>
<form
id="{{ panel_id }}-form"
class="compose-form"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'compose_send' %}"
hx-target="#{{ panel_id }}-status"
hx-swap="innerHTML">
<input type="hidden" name="service" value="{{ service }}">
<input type="hidden" name="identifier" value="{{ identifier }}">
<input type="hidden" name="render_mode" value="{{ render_mode }}">
<input type="hidden" name="limit" value="{{ limit }}">
{% if person %}
<input type="hidden" name="person" value="{{ person.id }}">
{% endif %}
<div class="compose-composer-capsule">
<textarea
id="{{ panel_id }}-textarea"
class="compose-textarea"
name="text"
rows="1"
placeholder="Type a message. Enter to send, Shift+Enter for newline."></textarea>
<button class="button is-link is-light compose-send-btn" type="submit">
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>Send</span>
</button>
</div>
</form>
</div>
<style>
#{{ panel_id }}.compose-shell {
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 8px;
box-shadow: none;
padding: 0.7rem;
background: #fff;
}
#{{ panel_id }} .compose-thread {
margin-top: 0.55rem;
margin-bottom: 0.55rem;
min-height: 16rem;
max-height: 62vh;
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
padding: 0.65rem;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.7), rgba(255, 255, 255, 0.98));
}
#{{ panel_id }} .compose-row {
display: flex;
margin-bottom: 0.5rem;
}
#{{ panel_id }} .compose-row.is-in {
justify-content: flex-start;
}
#{{ panel_id }} .compose-row.is-out {
justify-content: flex-end;
}
#{{ panel_id }} .compose-bubble {
max-width: min(85%, 46rem);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.13);
padding: 0.52rem 0.62rem;
box-shadow: none;
}
#{{ panel_id }} .compose-bubble.is-in {
background: rgba(255, 255, 255, 0.96);
}
#{{ panel_id }} .compose-bubble.is-out {
background: #eef6ff;
}
#{{ panel_id }} .compose-body {
margin: 0 0 0.2rem 0;
white-space: pre-wrap;
word-break: break-word;
}
#{{ panel_id }} .compose-msg-meta,
#{{ panel_id }} .compose-meta-line {
color: #616161;
font-size: 0.72rem;
}
#{{ panel_id }} .compose-msg-meta {
margin: 0;
}
#{{ panel_id }} .compose-empty {
margin: 0;
color: #6f6f6f;
font-size: 0.78rem;
}
#{{ panel_id }} .compose-composer-capsule {
display: flex;
align-items: flex-end;
gap: 0.45rem;
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 8px;
background: #fff;
padding: 0.35rem;
}
#{{ panel_id }} .compose-textarea {
flex: 1 1 auto;
min-height: 2.45rem;
max-height: 8rem;
resize: none;
border: none;
box-shadow: none;
outline: none;
background: transparent;
line-height: 1.35;
font-size: 0.98rem;
padding: 0.45rem 0.5rem;
}
#{{ panel_id }} .compose-send-btn {
height: 2.45rem;
border-radius: 8px;
margin: 0;
}
#{{ panel_id }} .compose-status {
margin-top: 0.55rem;
min-height: 1.1rem;
}
@media (max-width: 768px) {
#{{ panel_id }} .compose-thread {
max-height: 52vh;
}
#{{ panel_id }} .compose-send-btn span:last-child {
display: none;
}
}
</style>
<script>
(function () {
const panelId = "{{ panel_id }}";
const panel = document.getElementById(panelId);
if (!panel) {
return;
}
const thread = document.getElementById(panelId + "-thread");
const form = document.getElementById(panelId + "-form");
const textarea = document.getElementById(panelId + "-textarea");
if (!thread || !form || !textarea) {
return;
}
window.giaComposePanels = window.giaComposePanels || {};
const previousState = window.giaComposePanels[panelId];
if (previousState && previousState.timer) {
clearInterval(previousState.timer);
}
if (previousState && previousState.eventHandler) {
document.body.removeEventListener("composeMessageSent", previousState.eventHandler);
}
const panelState = { timer: null, polling: false };
window.giaComposePanels[panelId] = panelState;
const toInt = function (value) {
const parsed = parseInt(value || "0", 10);
return Number.isFinite(parsed) ? parsed : 0;
};
let lastTs = toInt(thread.dataset.lastTs);
const autosize = function () {
textarea.style.height = "auto";
const targetHeight = Math.min(Math.max(textarea.scrollHeight, 40), 128);
textarea.style.height = targetHeight + "px";
};
textarea.addEventListener("input", autosize);
autosize();
const nearBottom = function () {
return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 100;
};
const scrollToBottom = function (force) {
if (force || nearBottom()) {
thread.scrollTop = thread.scrollHeight;
}
};
const appendBubble = function (msg) {
const row = document.createElement("div");
const outgoing = !!msg.outgoing;
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
row.dataset.ts = String(msg.ts || 0);
const bubble = document.createElement("article");
bubble.className = "compose-bubble " + (outgoing ? "is-out" : "is-in");
const body = document.createElement("p");
body.className = "compose-body";
body.textContent = String(msg.text || "(no text)");
bubble.appendChild(body);
const meta = document.createElement("p");
meta.className = "compose-msg-meta";
let metaText = String(msg.display_ts || msg.ts || "");
if (msg.author) {
metaText += " · " + String(msg.author);
}
meta.textContent = metaText;
bubble.appendChild(meta);
row.appendChild(bubble);
const empty = thread.querySelector(".compose-empty");
if (empty) {
empty.remove();
}
thread.appendChild(row);
};
const poll = async function (forceScroll) {
if (panelState.polling) {
return;
}
panelState.polling = true;
try {
const params = new URLSearchParams();
params.set("service", thread.dataset.service || "");
params.set("identifier", thread.dataset.identifier || "");
if (thread.dataset.person) {
params.set("person", thread.dataset.person);
}
params.set("limit", thread.dataset.limit || "60");
params.set("after_ts", String(lastTs));
const url = thread.dataset.pollUrl + "?" + params.toString();
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" },
});
if (!response.ok) {
return;
}
const payload = await response.json();
const messages = Array.isArray(payload.messages) ? payload.messages : [];
const shouldStick = nearBottom() || forceScroll;
messages.forEach(function (msg) {
appendBubble(msg);
lastTs = Math.max(lastTs, toInt(msg.ts));
});
if (payload.last_ts !== undefined && payload.last_ts !== null) {
lastTs = Math.max(lastTs, toInt(payload.last_ts));
}
thread.dataset.lastTs = String(lastTs);
if (messages.length > 0) {
scrollToBottom(shouldStick);
}
} catch (err) {
console.debug("compose poll error", err);
} finally {
panelState.polling = false;
}
};
textarea.addEventListener("keydown", function (event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
form.requestSubmit();
}
});
form.addEventListener("htmx:afterRequest", function (event) {
if (event.detail && event.detail.successful) {
textarea.value = "";
autosize();
poll(true);
textarea.focus();
}
});
panelState.eventHandler = function () {
poll(true);
};
document.body.addEventListener("composeMessageSent", panelState.eventHandler);
scrollToBottom(true);
panelState.timer = setInterval(function () {
if (!document.getElementById(panelId)) {
clearInterval(panelState.timer);
document.body.removeEventListener(
"composeMessageSent",
panelState.eventHandler
);
delete window.giaComposePanels[panelId];
return;
}
poll(false);
}, 1800);
})();
</script>

View File

@@ -0,0 +1,5 @@
{% if notice_message %}
<article class="notification is-{{ notice_level|default:'info' }} is-light" style="padding: 0.45rem 0.65rem; margin: 0;">
{{ notice_message }}
</article>
{% endif %}

View File

@@ -17,6 +17,10 @@
<th>sender</th>
<th>text</th>
<th>author</th>
<th>delivered ts</th>
<th>read ts</th>
<th>read service</th>
<th>read by</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@@ -43,6 +47,10 @@
</td>
<td>{{ item.text }}</td>
<td>{{ item.custom_author }}</td>
<td>{{ item.delivered_ts }}</td>
<td>{{ item.read_ts }}</td>
<td>{{ item.read_source_service }}</td>
<td>{{ item.read_by_identifier }}</td>
<td>
<div class="buttons">
<button
@@ -78,4 +86,4 @@
{% endfor %}
</table>
{% endcache %}
{% endcache %}

View File

@@ -0,0 +1,13 @@
{% if items %}
{% for item in items %}
<a class="navbar-item" href="{{ item.compose_url }}">
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span style="margin-left: 0.35rem;">
{{ item.person_name }} · {{ item.service|title }}
</span>
</a>
{% endfor %}
{% else %}
<a class="navbar-item is-disabled">No contacts found.</a>
{% endif %}

View File

@@ -14,6 +14,7 @@
<th>id</th>
<th>identifier</th>
<th>last interaction</th>
<th>summary</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@@ -29,6 +30,7 @@
</td>
<td>{{ item.identifier }}</td>
<td>{{ item.last_interaction }}</td>
<td>{{ item.summary|default:"" }}</td>
<td>
<div class="buttons">
<button
@@ -73,4 +75,4 @@
{% endfor %}
</table>
{% endcache %}
{% endcache %}

View File

@@ -1,2 +1 @@
<img src="data:image/png;base64, {{ object }}" alt="Signal QR code" />
<img src="data:image/png;base64, {{ object }}" alt="Service QR code" />

View File

@@ -1,6 +1,11 @@
{% load cache %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_signal_accounts request.user.id object_list type %}
{% cache 600 objects_signal_accounts request.user.id object_list type service %}
{% if service_warning %}
<article class="notification is-warning is-light" style="margin-bottom: 0.55rem;">
{{ service_warning }}
</article>
{% endif %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
@@ -9,7 +14,7 @@
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>number</th>
<th>{{ service_label|default:"Service" }} account</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@@ -31,52 +36,54 @@
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'signal_contacts' type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
{% if show_contact_actions %}
{% if type == 'page' %}
<a href="{% url 'signal_contacts' type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</span>
</button>
</a>
<a href="{% url 'signal_chats' type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
</button>
</a>
<a href="{% url 'signal_chats' type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
</span>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_contacts' type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_contacts' type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_chats' type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_chats' type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
</span>
</span>
</span>
</button>
</button>
{% endif %}
{% endif %}
</div>
</td>
@@ -86,19 +93,17 @@
</table>
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'signal_account_add' type=type %}"
hx-target="#modals-here"
hx-post="{% url account_add_url_name type=type %}"
hx-target="#widgets-here"
hx-swap="innerHTML">
{% csrf_token %}
<div class="field has-addons">
<div id="device" class="control is-expanded has-icons-left">
<input
hx-post="{% url 'signal_account_add' type=type %}"
hx-target="#widgets-here"
hx-swap="innerHTML"
name="device"
class="input"
type="text"
required
placeholder="Account name">
<span class="icon is-small is-left">
<i class="fa-solid fa-plus"></i>
@@ -108,15 +113,13 @@
<div class="field">
<button
id="search"
type="submit"
class="button is-fullwidth"
hx-post="{% url 'signal_account_add' type=type %}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="innerHTML">
>
Add account
</button>
</div>
</div>
</div>
</form>
{% endcache %}
{% endcache %}

View File

@@ -13,22 +13,24 @@
<th>uuid</th>
<th>account</th>
<th>name</th>
<th>person</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.source_number }}</td>
<td>{{ item.chat.source_number }}</td>
<td>
<a
class="has-text-grey button nowrap-child"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.source_uuid }}');">
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.chat.source_uuid }}');">
<span class="icon" data-tooltip="Copy to clipboard">
<i class="fa-solid fa-copy" aria-hidden="true"></i>
</span>
</a>
</td>
<td>{{ item.account }}</td>
<td>{{ item.source_name }}</td>
<td>{{ item.chat.account }}</td>
<td>{{ item.chat.source_name }}</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
<div class="buttons">
<button
@@ -37,7 +39,7 @@
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to unlink {{ item }}?"
hx-confirm="Are you sure you wish to unlink {{ item.chat }}?"
class="button">
<span class="icon-text">
<span class="icon">
@@ -46,51 +48,67 @@
</span>
</button>
{% if type == 'page' %}
<a href="{# url 'signal_contacts' type=type pk=item #}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
{% if item.can_compose %}
<a href="{{ item.compose_page_url }}"><button
class="button"
title="Manual text mode">
<span class="icon-text">
<span class="icon">
<i class="{{ item.manual_icon_class }}"></i>
</span>
</span>
</span>
</button>
</a>
<a href="{# url 'signal_chats' type=type pk=item #}"><button
class="button">
</button>
</a>
{% else %}
<button class="button" disabled title="No identifier available for manual send">
<span class="icon-text">
<span class="icon">
<i class="{{ item.manual_icon_class }}"></i>
</span>
</span>
</button>
{% endif %}
<a href="{{ item.ai_url }}"><button
class="button"
title="Open AI workspace">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
<i class="fa-solid fa-brain-circuit"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'signal_contacts' type=type pk=item #}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
{% if item.can_compose %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ item.compose_widget_url }}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="afterend"
class="button">
<span class="icon-text">
<span class="icon">
<i class="{{ item.manual_icon_class }}"></i>
</span>
</span>
</button>
{% else %}
<button class="button" disabled title="No identifier available for manual send">
<span class="icon-text">
<span class="icon">
<i class="{{ item.manual_icon_class }}"></i>
</span>
</span>
</button>
{% endif %}
<a href="{{ item.ai_url }}"><button class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-brain-circuit"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'signal_chats' type=type pk=item #}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-envelope"></i>
</span>
</span>
</button>
</button></a>
{% endif %}
</div>
</td>
@@ -98,4 +116,4 @@
{% endfor %}
</table>
{% endcache %}
{% endcache %}