Improve and condense related controls

This commit is contained in:
2026-02-15 22:11:17 +00:00
parent ae3365e165
commit 981ee56de7
18 changed files with 1340 additions and 209 deletions

View File

@@ -190,6 +190,26 @@
.modal-background{
background-color:rgba(255, 255, 255, 0.3) !important;
}
#modals-here .modal-background {
background-color: rgba(0, 0, 0, 0.34) !important;
}
#modals-here .modal-content > .box {
background-color: rgba(255, 255, 255, 0.97) !important;
color: inherit;
}
#modals-here .modal-content .input,
#modals-here .modal-content .textarea,
#modals-here .modal-content .select select {
background-color: rgba(255, 255, 255, 0.98) !important;
}
[data-theme="dark"] #modals-here .modal-content > .box {
background-color: rgba(45, 45, 45, 0.97) !important;
}
[data-theme="dark"] #modals-here .modal-content .input,
[data-theme="dark"] #modals-here .modal-content .textarea,
[data-theme="dark"] #modals-here .modal-content .select select {
background-color: rgba(33, 33, 33, 0.98) !important;
}
.has-background-grey-lighter{
background-color:rgba(219, 219, 219, 0.5) !important;
@@ -340,25 +360,9 @@
Queue
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
OSINT
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'people' type='page' %}">
People
</a>
<a class="navbar-item" href="{% url 'groups' type='page' %}">
Groups
</a>
<a class="navbar-item" href="{% url 'personas' type='page' %}">
Personas
</a>
<a class="navbar-item" href="{% url 'manipulations' type='page' %}">
Manipulations
</a>
</div>
</div>
<a class="navbar-item" href="{% url 'osint_workspace' %}">
OSINT
</a>
{% endif %}
<a class="navbar-item add-button">
Install
@@ -464,6 +468,54 @@
});
});
});
window.giaPrepareWidgetTarget = function () {
const target = document.getElementById("widgets-here");
if (target) {
target.style.display = "block";
}
};
window.giaCanSpawnWidgets = function () {
return !!(
window.grid &&
typeof window.grid.addWidget === "function" &&
document.getElementById("grid-stack-main") &&
document.getElementById("widgets-here")
);
};
window.giaEnableWidgetSpawnButtons = function (root) {
const scope = root && root.querySelectorAll ? root : document;
const canSpawn = window.giaCanSpawnWidgets();
scope.querySelectorAll(".js-widget-spawn-trigger").forEach(function (button) {
const widgetUrl = String(
button.getAttribute("data-widget-url")
|| button.getAttribute("hx-get")
|| ""
).trim();
const visible = canSpawn && !!widgetUrl;
button.classList.toggle("is-hidden", !visible);
button.setAttribute("aria-hidden", visible ? "false" : "true");
});
};
document.addEventListener("click", function (event) {
const trigger = event.target.closest(".js-widget-spawn-trigger");
if (!trigger) {
return;
}
window.giaPrepareWidgetTarget();
});
document.addEventListener("DOMContentLoaded", function () {
window.giaEnableWidgetSpawnButtons(document);
});
document.body.addEventListener("htmx:afterSwap", function (event) {
const target = (event && event.target) || document;
window.giaEnableWidgetSpawnButtons(target);
});
</script>
{% block outer_content %}
{% endblock %}

View File

@@ -50,6 +50,9 @@
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
if (typeof window.giaEnableWidgetSpawnButtons === "function") {
window.giaEnableWidgetSpawnButtons(widgetelement);
}
// update the size of the widget according to its content
var added_widget = htmx.find(grid_element, "#"+new_id);
@@ -77,6 +80,9 @@
// container.inner = "";
// }
grid.compact();
if (typeof window.giaEnableWidgetSpawnButtons === "function") {
window.giaEnableWidgetSpawnButtons(document);
}
});
</script>
<div>

View File

@@ -0,0 +1,11 @@
{% extends "index.html" %}
{% block load_widgets %}
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ tabs_widget_url }}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
{% endblock %}

View File

@@ -8,49 +8,64 @@
{% endif %}
</div>
<div class="is-flex is-flex-direction-column mitigation-header-meta" 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
class="box mitigation-artifact-card mitigation-editable-shell"
style="padding: 0.45rem; margin: 0; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;"
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 class="mitigation-artifact-headline">
<p class="mitigation-artifact-title">Plan Details</p>
<div class="mitigation-artifact-actions">
<button
type="button"
class="button is-small is-link is-light is-rounded mitigation-edit-btn"
data-edit-state="view"
title="Edit plan details"
onclick="giaMitigationToggleEdit(this); return false;">
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
</button>
</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>
<p class="mitigation-artifact-meta">Created {{ plan.created_at }} · Updated {{ plan.updated_at }}</p>
<p class="mitigation-artifact-preview">
{{ plan.creation_mode|title }} / {{ plan.status|title }}
{% if plan.source_ai_result_id %}
· Source Result {{ plan.source_ai_result_id }}
{% endif %}
</p>
<div class="mitigation-edit-fields">
<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" data-editable="1" readonly>
</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 class="field" style="margin-bottom: 0.3rem;">
<div class="control">
<textarea class="textarea is-small" rows="2" name="objective" placeholder="Plan objective" data-editable="1" readonly>{{ plan.objective }}</textarea>
</div>
</div>
<div class="control">
<button type="submit" class="button is-small is-light">Save</button>
<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" data-editable-toggle="1" disabled>
{% 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" data-editable-toggle="1" disabled>
{% for value, label in plan_status_choices %}
<option value="{{ value }}" {% if plan.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</form>
@@ -263,73 +278,88 @@
{% if corrections %}
{% 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>
<article class="box mitigation-artifact-card mitigation-editable-shell" style="padding: 0.45rem; margin-bottom: 0.35rem; border: 1px solid rgba(0, 0, 0, 0.12); box-shadow: none;">
<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 %}"
hx-target="#mitigation-shell-{{ person.id }}"
hx-swap="outerHTML">
<div class="columns is-multiline" style="margin: 0 -0.3rem;">
<div class="column is-12" style="padding: 0.3rem;">
<input class="input is-small" type="text" name="title" value="{{ correction.title }}">
</div>
<div class="column is-12" style="padding: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Message Context</label>
<textarea class="textarea is-small" rows="2" name="source_phrase">{{ correction.source_phrase }}</textarea>
</div>
<div class="column is-12" style="padding: 0.3rem;">
<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>
<input type="hidden" name="active_tab" value="{{ active_tab|default:'corrections' }}">
<div class="mitigation-artifact-headline">
<p class="mitigation-artifact-title">{{ correction.title }}</p>
<div class="mitigation-artifact-actions">
<label class="checkbox is-size-7 mitigation-artifact-enabled">
<input type="checkbox" name="enabled" value="1" data-editable-toggle="1" disabled {% if correction.enabled %}checked{% endif %}>
On
</label>
<button
type="button"
class="button is-small is-link is-light is-rounded mitigation-edit-btn"
data-edit-state="view"
title="Edit correction"
onclick="giaMitigationToggleEdit(this); return false;">
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
</button>
<button
type="button"
class="button is-small is-danger is-light is-rounded"
title="Delete correction"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_artifact_delete' type='widget' person_id=person.id plan_id=plan.id kind='correction' artifact_id=correction.id %}"
hx-vals='{"active_tab":"corrections"}'
hx-confirm="Delete this correction?"
hx-target="#mitigation-shell-{{ person.id }}"
hx-swap="outerHTML">
<span class="icon is-small"><i class="fa-solid fa-trash"></i></span>
</button>
</div>
</div>
<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>
<button
type="button"
class="button is-small is-danger is-light"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'ai_workspace_mitigation_artifact_delete' type='widget' person_id=person.id plan_id=plan.id kind='correction' artifact_id=correction.id %}"
hx-vals='{"active_tab":"corrections"}'
hx-confirm="Delete this correction?"
hx-target="#mitigation-shell-{{ person.id }}"
hx-swap="outerHTML">Delete</button>
<p class="mitigation-artifact-meta">Created {{ correction.created_at }}</p>
<p class="mitigation-artifact-preview">{{ correction.clarification }}</p>
<div class="mitigation-edit-fields">
<div class="field" style="margin-bottom: 0.3rem;">
<input class="input is-small" type="text" name="title" value="{{ correction.title }}" data-editable="1" readonly>
</div>
<div class="field" style="margin-bottom: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Message Context</label>
<textarea class="textarea is-small" rows="2" name="source_phrase" data-editable="1" readonly>{{ correction.source_phrase }}</textarea>
</div>
<div class="field" style="margin-bottom: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.2rem;">Insight</label>
<textarea class="textarea is-small" rows="2" name="body" data-editable="1" readonly>{{ correction.clarification }}</textarea>
</div>
<div class="columns is-multiline" style="margin: 0 -0.3rem;">
<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" data-editable-toggle="1" disabled>
{% 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" data-editable-toggle="1" disabled>
{% 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" data-editable-toggle="1" disabled>
{% 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>
</div>
</form>
</article>
@@ -511,18 +541,33 @@
<div class="columns is-multiline" style="margin: 0 -0.3rem;">
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="checkbox is-size-7"><input type="checkbox" name="enabled" value="1" {% if auto_settings.enabled %}checked{% endif %}> Enable auto checks for this Conversation</label>
<p class="is-size-7 has-text-grey" style="margin-top: 0.2rem; margin-bottom: 0;">
Master gate. When off, automatic checks return early and no auto plan, correction, or notification actions run for this conversation.
</p>
</div>
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="checkbox is-size-7"><input type="checkbox" name="auto_pattern_recognition" value="1" {% if auto_settings.auto_pattern_recognition %}checked{% endif %}> Detect pattern signals from Message rows</label>
<p class="is-size-7 has-text-grey" style="margin-top: 0.2rem; margin-bottom: 0;">
Controls background trigger behavior only. If disabled, auto-triggered scans are skipped; manual "Run Check Now" can still run.
</p>
</div>
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="checkbox is-size-7"><input type="checkbox" name="auto_create_mitigation" value="1" {% if auto_settings.auto_create_mitigation %}checked{% endif %}> Create a Plan when the Conversation has none</label>
<p class="is-size-7 has-text-grey" style="margin-top: 0.2rem; margin-bottom: 0;">
On AI pane load, if no plan exists and automation is enabled, a baseline plan is auto-created from recent messages.
</p>
</div>
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="checkbox is-size-7"><input type="checkbox" name="auto_create_corrections" value="1" {% if auto_settings.auto_create_corrections %}checked{% endif %}> Create Correction rows linked to the Plan</label>
<p class="is-size-7 has-text-grey" style="margin-top: 0.2rem; margin-bottom: 0;">
Writes up to 8 detected correction candidates per run, deduplicated by title + clarification, and links them to this plan.
</p>
</div>
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="checkbox is-size-7"><input type="checkbox" name="auto_notify_enabled" value="1" {% if auto_settings.auto_notify_enabled %}checked{% endif %}> Notify when auto writes new Correction rows</label>
<p class="is-size-7 has-text-grey" style="margin-top: 0.2rem; margin-bottom: 0;">
Sends a notification when violations are found (with count + top preview), using NTFY overrides if provided, otherwise default notifications.
</p>
</div>
<div class="column is-12-mobile is-6-tablet" style="padding: 0.3rem;">
<label class="label is-small" style="margin-bottom: 0.25rem;">Message rows per check</label>

View File

@@ -76,6 +76,21 @@
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>Manual Text Mode</span>
</a>
{% if compose_widget_url %}
<button
type="button"
class="button is-light is-small js-widget-spawn-trigger is-hidden"
data-widget-url="{{ compose_widget_url }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ compose_widget_url }}"
hx-target="#widgets-here"
hx-swap="afterend"
title="Open Manual Text widget here"
aria-label="Open Manual Text widget here">
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
<span>Widget</span>
</button>
{% endif %}
</div>
{% endif %}
</div>
@@ -113,37 +128,74 @@
<div class="ai-response-capsule" style="margin-bottom: 0.5rem; border: 1px solid rgba(0, 0, 0, 0.16); border-radius: 8px; padding: 0.5rem 0.6rem;">
<div class="is-flex is-justify-content-space-between is-align-items-center" style="margin-bottom: 0.4rem;">
<div class="tags" style="margin-bottom: 0.25rem;">
<span class="tag is-info is-light is-small">Mitigation</span>
<span class="tag is-info is-light is-small">Control</span>
<span class="tag is-warning is-light is-small">AI Output</span>
</div>
<div class="tabs is-small is-toggle is-toggle-rounded ai-top-tabs" style="margin-bottom: 0;">
<ul>
<li id="ai-tab-{{ person.id }}-plan_board" class="is-active ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'plan_board', false); return false;">Plan</a>
<a
title="Control plan board"
aria-label="Control plan board"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'plan_board', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-diagram-project"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-corrections" class="ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'corrections', false); return false;">Corrections</a>
<a
title="Corrections"
aria-label="Corrections"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'corrections', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-screwdriver-wrench"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-engage" class="ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'engage', false); return false;">Engage</a>
<a
title="Engage"
aria-label="Engage"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'engage', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-paper-plane"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-fundamentals" class="ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'fundamentals', false); return false;">Fundamentals</a>
</li>
<li id="ai-tab-{{ person.id }}-auto" class="ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'auto', false); return false;">Auto</a>
<a
title="Fundamentals"
aria-label="Fundamentals"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'fundamentals', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-book-open"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-ask_ai" class="ai-top-tab-mitigation">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'ask_ai', false); return false;">Ask AI</a>
<a
title="Ask AI"
aria-label="Ask AI"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'ask_ai', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-robot"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-summarise" class="ai-top-tab-output ai-top-tab-output-start">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'summarise', false); return false;">Summary</a>
<a
title="Summary"
aria-label="Summary"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'summarise', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-file-lines"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-draft_reply" class="ai-top-tab-output">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'draft_reply', false); return false;">Draft</a>
<a
title="Draft"
aria-label="Draft"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'draft_reply', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-pen-to-square"></i></span>
</a>
</li>
<li id="ai-tab-{{ person.id }}-extract_patterns" class="ai-top-tab-output">
<a onclick="giaWorkspaceOpenTab('{{ person.id }}', 'extract_patterns', false); return false;">Patterns</a>
<a
title="Patterns"
aria-label="Patterns"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'extract_patterns', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
</a>
</li>
</ul>
</div>
@@ -208,14 +260,18 @@
display: flex;
align-items: center;
justify-content: center;
min-height: 2.55rem;
min-height: 2.2rem;
min-width: 2.2rem;
padding: 0 0.55rem;
line-height: 1.15;
text-align: center;
white-space: normal;
white-space: nowrap;
}
@media screen and (max-width: 768px) {
.ai-person-widget .ai-top-tabs li a {
min-height: 2.8rem;
min-height: 2.1rem;
min-width: 2.1rem;
padding: 0 0.45rem;
}
}
</style>
@@ -356,7 +412,7 @@
}
const OPERATION_TABS = ["summarise", "draft_reply", "extract_patterns"];
const MITIGATION_TABS = ["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"];
const MITIGATION_TABS = ["plan_board", "corrections", "engage", "fundamentals", "ask_ai"];
const ALL_TOP_TABS = MITIGATION_TABS.concat(OPERATION_TABS);
function isMitigationTab(tabKey) {
@@ -483,6 +539,7 @@
if (cacheAllowed && !forceRefresh && entry) {
pane.innerHTML = entry.html;
pane.dataset.loaded = "1";
executeInlineScripts(pane);
pane.classList.remove("ai-animate-in");
void pane.offsetWidth;
pane.classList.add("ai-animate-in");
@@ -649,7 +706,7 @@
if (typeof window.giaMitigationShowTab !== "function") {
window.giaMitigationShowTab = function(pid, tabName) {
const names = ["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"];
const names = ["plan_board", "corrections", "engage", "fundamentals", "ask_ai"];
names.forEach(function(name) {
const pane = document.getElementById("mitigation-tab-" + pid + "-" + name);
const tab = document.getElementById("mitigation-tab-btn-" + pid + "-" + name);

View File

@@ -34,6 +34,21 @@
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span>AI Workspace</span>
</a>
{% if ai_workspace_widget_url %}
<button
type="button"
class="button is-light is-rounded is-small js-widget-spawn-trigger is-hidden"
data-widget-url="{{ ai_workspace_widget_url }}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ ai_workspace_widget_url }}"
hx-target="#widgets-here"
hx-swap="afterend"
title="Open AI Person widget here"
aria-label="Open AI Person widget here">
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
<span>Widget</span>
</button>
{% endif %}
{% if render_mode == "page" %}
<a class="button is-light is-rounded" href="{{ compose_workspace_url }}">
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
@@ -109,6 +124,32 @@
</div>
</div>
</div>
<div id="{{ panel_id }}-lightbox" class="compose-lightbox is-hidden" aria-hidden="true">
<button
type="button"
id="{{ panel_id }}-lightbox-prev"
class="compose-lightbox-nav compose-lightbox-prev"
aria-label="Previous image">
<span class="icon is-small"><i class="fa-solid fa-chevron-left"></i></span>
</button>
<button type="button" class="compose-lightbox-close" aria-label="Close image preview">
<span class="icon is-small"><i class="fa-solid fa-xmark"></i></span>
</button>
<figure class="compose-lightbox-frame">
<img
id="{{ panel_id }}-lightbox-image"
class="compose-lightbox-image"
src=""
alt="Conversation attachment preview">
</figure>
<button
type="button"
id="{{ panel_id }}-lightbox-next"
class="compose-lightbox-nav compose-lightbox-next"
aria-label="Next image">
<span class="icon is-small"><i class="fa-solid fa-chevron-right"></i></span>
</button>
</div>
<div
id="{{ panel_id }}-glance"
@@ -340,6 +381,86 @@
border: 1px solid rgba(0, 0, 0, 0.14);
object-fit: cover;
background: #f8f8f8;
cursor: zoom-in;
transition: filter 120ms ease;
}
#{{ panel_id }} .compose-image:hover {
filter: brightness(0.97);
}
#{{ panel_id }}-lightbox.compose-lightbox {
position: fixed;
inset: 0;
z-index: 160;
background: rgba(10, 12, 16, 0.82);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
#{{ panel_id }}-lightbox.compose-lightbox.is-hidden {
display: none;
}
#{{ panel_id }}-lightbox .compose-lightbox-frame {
margin: 0;
max-width: min(96vw, 70rem);
max-height: 88vh;
display: flex;
align-items: center;
justify-content: center;
}
#{{ panel_id }}-lightbox .compose-lightbox-image {
display: block;
max-width: min(96vw, 70rem);
max-height: 88vh;
width: auto;
height: auto;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
object-fit: contain;
background: #111;
}
#{{ panel_id }}-lightbox .compose-lightbox-close {
position: absolute;
top: 0.8rem;
right: 0.8rem;
width: 2rem;
height: 2rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.24);
background: rgba(10, 12, 16, 0.62);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#{{ panel_id }}-lightbox .compose-lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
width: 2rem;
height: 2rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.24);
background: rgba(10, 12, 16, 0.62);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: opacity 120ms ease-out;
}
#{{ panel_id }}-lightbox .compose-lightbox-prev {
left: 0.8rem;
}
#{{ panel_id }}-lightbox .compose-lightbox-next {
right: 0.8rem;
}
#{{ panel_id }}-lightbox .compose-lightbox-nav:disabled {
opacity: 0.3;
cursor: default;
}
#{{ panel_id }} .compose-body {
margin: 0 0 0.2rem 0;
@@ -687,6 +808,12 @@
#{{ panel_id }} .compose-qi-chip .k {
color: #657283;
font-size: 0.67rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
#{{ panel_id }} .compose-qi-chip .v {
font-size: 0.78rem;
@@ -735,6 +862,25 @@
overflow-wrap: anywhere;
word-break: break-word;
}
#{{ panel_id }} .compose-qi-doc-dot {
width: 0.5rem;
height: 0.5rem;
min-width: 0.5rem;
border-radius: 50%;
border: 0;
padding: 0;
margin: 0;
background: #7a94b4;
cursor: help;
opacity: 0.85;
transform: translateY(0.02rem);
}
#{{ panel_id }} .compose-qi-doc-dot:hover,
#{{ panel_id }} .compose-qi-doc-dot:focus-visible {
opacity: 1;
outline: 1px solid rgba(52, 101, 164, 0.45);
outline-offset: 1px;
}
#{{ panel_id }} .compose-qi-row-meta {
display: inline-flex;
align-items: center;
@@ -870,7 +1016,14 @@
const glanceNode = document.getElementById(panelId + "-glance");
const popover = document.getElementById(panelId + "-popover");
const popoverBackdrop = document.getElementById(panelId + "-popover-backdrop");
const lightbox = document.getElementById(panelId + "-lightbox");
const lightboxImage = document.getElementById(panelId + "-lightbox-image");
const lightboxPrev = document.getElementById(panelId + "-lightbox-prev");
const lightboxNext = document.getElementById(panelId + "-lightbox-next");
const csrfToken = "{{ csrf_token }}";
if (lightbox && lightbox.parentElement !== document.body) {
document.body.appendChild(lightbox);
}
window.giaComposePanels = window.giaComposePanels || {};
const previousState = window.giaComposePanels[panelId];
@@ -889,13 +1042,19 @@
if (previousState && previousState.resizeHandler) {
window.removeEventListener("resize", previousState.resizeHandler);
}
if (previousState && previousState.lightboxKeyHandler) {
document.removeEventListener("keydown", previousState.lightboxKeyHandler);
}
const panelState = {
timer: null,
polling: false,
socket: null,
websocketReady: false,
activePanel: null,
engageToken: ""
engageToken: "",
lightboxKeyHandler: null,
lightboxImages: [],
lightboxIndex: -1,
};
window.giaComposePanels[panelId] = panelState;
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
@@ -942,6 +1101,84 @@
}
return String(Math.floor(ts / 60000));
};
const collectLightboxImages = function () {
return Array.from(thread.querySelectorAll(".compose-image"));
};
const syncLightboxNav = function () {
const total = panelState.lightboxImages.length;
const index = panelState.lightboxIndex;
if (lightboxPrev) {
lightboxPrev.disabled = total < 2 || index <= 0;
}
if (lightboxNext) {
lightboxNext.disabled = total < 2 || index >= (total - 1);
}
};
const openLightboxAt = function (index) {
if (!lightbox || !lightboxImage) {
return;
}
const images = collectLightboxImages();
if (!images.length) {
return;
}
const safeIndex = Math.max(0, Math.min(Number(index) || 0, images.length - 1));
const imageNode = images[safeIndex];
const source = String(imageNode.currentSrc || imageNode.src || "").trim();
if (!source) {
return;
}
panelState.lightboxImages = images;
panelState.lightboxIndex = safeIndex;
lightboxImage.src = source;
lightbox.classList.remove("is-hidden");
lightbox.setAttribute("aria-hidden", "false");
syncLightboxNav();
};
const openLightboxFromElement = function (imageNode) {
const images = collectLightboxImages();
if (!images.length) {
return;
}
const idx = images.indexOf(imageNode);
openLightboxAt(idx >= 0 ? idx : 0);
};
const stepLightbox = function (delta) {
if (!lightbox || lightbox.classList.contains("is-hidden")) {
return;
}
if (!panelState.lightboxImages.length) {
panelState.lightboxImages = collectLightboxImages();
}
if (!panelState.lightboxImages.length) {
return;
}
openLightboxAt(panelState.lightboxIndex + delta);
};
const closeLightbox = function () {
if (!lightbox) {
return;
}
lightbox.classList.add("is-hidden");
lightbox.setAttribute("aria-hidden", "true");
if (lightboxImage) {
lightboxImage.removeAttribute("src");
}
panelState.lightboxImages = [];
panelState.lightboxIndex = -1;
syncLightboxNav();
};
const openLightbox = function (srcValue) {
const source = String(srcValue || "").trim();
if (!source) {
return;
}
const images = collectLightboxImages();
const idx = images.findIndex(function (img) {
return String(img.currentSrc || img.src || "").trim() === source;
});
openLightboxAt(idx >= 0 ? idx : 0);
};
let lastTs = toInt(thread.dataset.lastTs);
let glanceState = {
@@ -1017,6 +1254,27 @@
if (!scope) {
return;
}
scope.querySelectorAll(".compose-image").forEach(function (img) {
if (img.dataset.lightboxBound === "1") {
return;
}
img.dataset.lightboxBound = "1";
img.setAttribute("role", "button");
img.setAttribute("tabindex", "0");
img.setAttribute("aria-label", "Open image preview");
img.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
openLightboxFromElement(img);
});
img.addEventListener("keydown", function (event) {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
openLightboxFromElement(img);
});
});
scope.querySelectorAll(".compose-bubble").forEach(function (bubble) {
const fallback = bubble.querySelector(".compose-image-fallback");
const refresh = function () {
@@ -1642,6 +1900,36 @@
const docs = Array.isArray(payload.docs) ? payload.docs : [];
container.innerHTML = "";
const docsTooltip = function (title, calculation, psychology) {
const parts = [];
if (calculation) {
parts.push("How it is calculated: " + String(calculation || ""));
}
if (psychology) {
parts.push("Psychological interpretation: " + String(psychology || ""));
}
if (!parts.length) {
return "";
}
return String(title || "Metric") + " | " + parts.join(" | ");
};
const appendDocDot = function (target, tooltipText, titleText) {
if (!target || !tooltipText) {
return;
}
const dot = document.createElement("button");
dot.type = "button";
dot.className = "compose-qi-doc-dot";
dot.title = String(tooltipText || "");
dot.setAttribute("aria-label", "Explain " + String(titleText || "metric"));
dot.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
});
target.appendChild(dot);
};
const stateFaceMeta = function (stateText) {
const state = String(stateText || "").toLowerCase();
if (state.includes("balanced")) {
@@ -1697,28 +1985,67 @@
const head = document.createElement("div");
head.className = "compose-qi-head";
[
{ key: "Platform", value: summary.platform || "-" },
{
key: "Platform",
value: summary.platform || "-",
docs: summary.platform_docs || {},
},
{
key: "Participant State",
value: summary.state || "-",
icon: stateFace.icon,
className: stateFace.className,
docs: summary.state_docs || {},
},
{
key: "Data Points",
value: String(summary.snapshot_count || 0),
docs: summary.snapshot_docs || {},
},
{
key: "Thread",
value: summary.thread || "-",
docs: summary.thread_docs || {},
},
{ key: "Data Points", value: String(summary.snapshot_count || 0) },
{ key: "Thread", value: summary.thread || "-" },
].forEach(function (pair) {
const chip = document.createElement("div");
chip.className = "compose-qi-chip";
let valueHtml = String(pair.value || "-");
const key = document.createElement("p");
key.className = "k";
const keyText = document.createElement("span");
keyText.textContent = String(pair.key || "");
key.appendChild(keyText);
const pairDocs = pair.docs || {};
appendDocDot(
key,
docsTooltip(
pair.key,
pairDocs.calculation,
pairDocs.psychology
),
pair.key
);
chip.appendChild(key);
const value = document.createElement("p");
value.className = "v";
if (pair.icon) {
valueHtml = (
'<span class="' + String(pair.className || "") + '">'
+ '<span class="icon is-small"><i class="' + String(pair.icon) + '"></i></span>'
+ "</span>"
+ "<span>" + valueHtml + "</span>"
);
const iconWrap = document.createElement("span");
iconWrap.className = String(pair.className || "");
const icon = document.createElement("span");
icon.className = "icon is-small";
const glyph = document.createElement("i");
glyph.className = String(pair.icon || "");
icon.appendChild(glyph);
iconWrap.appendChild(icon);
value.appendChild(iconWrap);
}
chip.innerHTML = '<p class="k">' + pair.key + "</p>" + '<p class="v">' + valueHtml + "</p>";
const valueText = document.createElement("span");
valueText.textContent = String(pair.value || "-");
value.appendChild(valueText);
chip.appendChild(value);
head.appendChild(chip);
});
container.appendChild(head);
@@ -1734,24 +2061,72 @@
rows.forEach(function (row) {
const node = document.createElement("article");
node.className = "compose-qi-row";
node.innerHTML = (
'<div class="compose-qi-row-head">'
+ '<p class="compose-qi-row-label"><span class="icon is-small"><i class="'
+ String(row.icon || "fa-solid fa-square") + '"></i></span><span>'
+ String(row.label || "") + "</span></p>"
+ '<p class="compose-qi-row-meta"><span>' + String(row.point_count || 0)
+ ' points</span><span class="' + String((row.trend || {}).class_name || "")
+ '"><span class="icon is-small"><i class="' + String((row.trend || {}).icon || "")
+ '"></i></span> ' + String(row.delta_label || "n/a")
+ "</span></p></div>"
+ '<div class="compose-qi-row-body">'
+ '<p class="compose-qi-value">' + String(row.display_value || "-") + "</p>"
+ '<p class="' + String(((row.emotion || {}).class_name) || "")
+ '" style="margin:0; font-size:0.72rem;">'
+ '<span class="icon is-small"><i class="' + String(((row.emotion || {}).icon) || "")
+ '"></i></span> ' + String(((row.emotion || {}).label) || "Unknown")
+ "</p></div>"
const rowHead = document.createElement("div");
rowHead.className = "compose-qi-row-head";
const rowLabel = document.createElement("p");
rowLabel.className = "compose-qi-row-label";
const rowIcon = document.createElement("span");
rowIcon.className = "icon is-small";
const rowIconGlyph = document.createElement("i");
rowIconGlyph.className = String(row.icon || "fa-solid fa-square");
rowIcon.appendChild(rowIconGlyph);
rowLabel.appendChild(rowIcon);
const rowLabelText = document.createElement("span");
rowLabelText.textContent = String(row.label || "");
rowLabel.appendChild(rowLabelText);
appendDocDot(
rowLabel,
docsTooltip(row.label, row.calculation, row.psychology),
row.label
);
rowHead.appendChild(rowLabel);
const rowMeta = document.createElement("p");
rowMeta.className = "compose-qi-row-meta";
const points = document.createElement("span");
points.textContent = String(row.point_count || 0) + " points";
rowMeta.appendChild(points);
const trend = row.trend || {};
const trendNode = document.createElement("span");
trendNode.className = String(trend.class_name || "");
const trendIcon = document.createElement("span");
trendIcon.className = "icon is-small";
const trendGlyph = document.createElement("i");
trendGlyph.className = String(trend.icon || "");
trendIcon.appendChild(trendGlyph);
trendNode.appendChild(trendIcon);
const trendText = document.createTextNode(" " + String(row.delta_label || "n/a"));
trendNode.appendChild(trendText);
rowMeta.appendChild(trendNode);
rowHead.appendChild(rowMeta);
node.appendChild(rowHead);
const rowBody = document.createElement("div");
rowBody.className = "compose-qi-row-body";
const rowValue = document.createElement("p");
rowValue.className = "compose-qi-value";
rowValue.textContent = String(row.display_value || "-");
rowBody.appendChild(rowValue);
const emotion = row.emotion || {};
const emotionNode = document.createElement("p");
emotionNode.className = String(emotion.class_name || "");
emotionNode.style.margin = "0";
emotionNode.style.fontSize = "0.72rem";
const emotionIconWrap = document.createElement("span");
emotionIconWrap.className = "icon is-small";
const emotionGlyph = document.createElement("i");
emotionGlyph.className = String(emotion.icon || "");
emotionIconWrap.appendChild(emotionGlyph);
emotionNode.appendChild(emotionIconWrap);
emotionNode.appendChild(
document.createTextNode(" " + String(emotion.label || "Unknown"))
);
rowBody.appendChild(emotionNode);
node.appendChild(rowBody);
list.appendChild(node);
});
container.appendChild(list);
@@ -1991,6 +2366,53 @@
hideAllCards();
});
}
if (lightbox) {
if (lightboxPrev) {
lightboxPrev.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
stepLightbox(-1);
});
}
if (lightboxNext) {
lightboxNext.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
stepLightbox(1);
});
}
const closeButton = lightbox.querySelector(".compose-lightbox-close");
if (closeButton) {
closeButton.addEventListener("click", function (event) {
event.preventDefault();
closeLightbox();
});
}
lightbox.addEventListener("click", function (event) {
if (event.target === lightbox) {
closeLightbox();
}
});
panelState.lightboxKeyHandler = function (event) {
if (lightbox.classList.contains("is-hidden")) {
return;
}
if (event.key === "Escape") {
closeLightbox();
return;
}
if (event.key === "ArrowLeft") {
event.preventDefault();
stepLightbox(-1);
return;
}
if (event.key === "ArrowRight") {
event.preventDefault();
stepLightbox(1);
}
};
document.addEventListener("keydown", panelState.lightboxKeyHandler);
}
panelState.resizeHandler = function () {
if (!popover || popover.classList.contains("is-hidden")) {
return;
@@ -2056,6 +2478,9 @@
document.body.removeEventListener("composeMessageSent", panelState.eventHandler);
document.body.removeEventListener("composeSendResult", panelState.sendResultHandler);
document.removeEventListener("mousedown", panelState.docClickHandler);
if (panelState.lightboxKeyHandler) {
document.removeEventListener("keydown", panelState.lightboxKeyHandler);
}
if (panelState.socket) {
try {
panelState.socket.close();
@@ -2063,6 +2488,9 @@
// Ignore.
}
}
if (lightbox && lightbox.parentElement === document.body) {
lightbox.remove();
}
delete window.giaComposePanels[panelId];
return;
}

View File

@@ -0,0 +1,71 @@
<div class="osint-workspace-tabs">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">
OSINT Workspace
</p>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.5rem;">
One-line setup capsule. Each tab opens a fresh setup widget.
</p>
<div class="osint-capsule-row" role="tablist" aria-label="OSINT setup tabs">
{% for tab in tabs %}
<button
type="button"
class="button osint-capsule-tab"
hx-get="{{ tab.widget_url }}"
hx-target="#widgets-here"
hx-swap="afterend"
onclick="document.getElementById('widgets-here').style.display='block';"
title="Open {{ tab.label }} setup widget">
<span class="icon is-small"><i class="{{ tab.icon }}"></i></span>
<span>{{ tab.label }}</span>
</button>
{% endfor %}
</div>
</div>
<style>
.osint-capsule-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.35rem;
width: 100%;
overflow-x: auto;
padding: 0.3rem;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.16);
background: linear-gradient(180deg, rgba(248, 250, 254, 0.95), rgba(255, 255, 255, 0.96));
}
.osint-capsule-tab {
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.14);
background: #fff;
box-shadow: none;
white-space: nowrap;
min-height: 2rem;
padding-left: 0.7rem;
padding-right: 0.7rem;
font-weight: 600;
}
.osint-capsule-tab:hover,
.osint-capsule-tab:focus-visible {
border-color: rgba(50, 115, 220, 0.5);
color: #2f67b2;
}
@media (max-width: 768px) {
.osint-capsule-row {
gap: 0.28rem;
padding: 0.25rem;
}
.osint-capsule-tab {
min-height: 1.82rem;
padding-left: 0.55rem;
padding-right: 0.55rem;
font-size: 0.76rem;
}
}
</style>

View File

@@ -70,6 +70,7 @@
onclick="return false;">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>{{ column.label }}</span>
<span class="is-size-7 has-text-grey ml-2">({{ column.field_name }})</span>
</a>
{% endfor %}
</div>
@@ -284,11 +285,18 @@
<script>
(function () {
const tableId = "{{ osint_table_id|escapejs }}";
const scopeKey = "{{ osint_scope|default:'global'|escapejs }}";
const columnSignature = "{{ osint_columns|length }}:{% for column in osint_columns %}{{ column.key|escapejs }}|{% endfor %}";
const shell = document.getElementById(tableId);
if (!shell) {
return;
}
const storageKey = "gia_osint_hidden_cols_v1:" + tableId;
const storageKey = [
"gia_osint_hidden_cols_v2",
tableId,
scopeKey,
columnSignature,
].join(":");
let hidden = [];
try {
hidden = JSON.parse(localStorage.getItem(storageKey) || "[]");

View File

@@ -1,14 +1,31 @@
{% if object.ok %}
<img src="data:image/png;base64, {{ object.image_b64 }}" alt="WhatsApp QR code" />
{% if object.warning %}
<p class="is-size-7" style="margin-top: 0.6rem;">{{ object.warning }}</p>
{% endif %}
{% else %}
<article class="notification is-warning is-light" style="margin-bottom: 0;">
<p><strong>WhatsApp QR Not Ready.</strong></p>
<p>{{ object.error|default:"No Neonize pairing QR is available yet." }}</p>
<div class="whatsapp-account-add-fragment">
{% if object.ok %}
<img src="data:image/png;base64, {{ object.image_b64 }}" alt="WhatsApp QR code" />
{% if object.warning %}
<p style="margin-top: 0.45rem;">{{ object.warning }}</p>
<p class="is-size-7" style="margin-top: 0.6rem;">{{ object.warning }}</p>
{% endif %}
</article>
{% endif %}
{% else %}
<article class="notification is-warning is-light" style="margin-bottom: 0;">
<p><strong>WhatsApp QR Not Ready.</strong></p>
<p>{{ object.error|default:"No Neonize pairing QR is available yet." }}</p>
{% if object.warning %}
<p style="margin-top: 0.45rem;">{{ object.warning }}</p>
{% endif %}
{% if object.pending %}
<p class="is-size-7" style="margin-top: 0.5rem;">
Waiting for Neonize QR event. This panel will refresh automatically.
</p>
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{{ detail_url }}"
hx-target="closest .whatsapp-account-add-fragment"
hx-swap="outerHTML"
hx-trigger="load delay:1800ms">
{% csrf_token %}
<input type="hidden" name="device" value="{{ object.device|default:'GIA Device' }}" />
<input type="hidden" name="refresh" value="1" />
</form>
{% endif %}
</article>
{% endif %}
</div>