Renovate mitigation panels and messages

This commit is contained in:
2026-02-15 21:02:40 +00:00
parent 63af5d234e
commit cc3fff0757
12 changed files with 1518 additions and 236 deletions

View File

@@ -1,4 +1,6 @@
<div style="margin-bottom: 0.5rem;">
<div
id="ai-result-links-{{ person.id }}-{{ operation }}-{{ ai_result_id|default:'new' }}"
style="margin-bottom: 0.5rem;">
<div class="tags has-addons" style="display: inline-flex; margin-bottom: 0.4rem;">
<span class="tag is-dark">
<i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
@@ -45,13 +47,32 @@
{% 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" %}
{% else %}
<div id="mitigation-shell-{{ person.id }}" class="box" style="padding: 0.65rem; margin-top: 0.2rem; border: 1px dashed rgba(0, 0, 0, 0.25); box-shadow: none;">
<p class="is-size-7 has-text-grey">No mitigation plan yet. Use the Patterns tab to generate one.</p>
<details
id="ai-mitigation-collapsible-{{ person.id }}"
class="ai-mitigation-collapsible"
open
style="margin-top: 0.35rem;">
<summary class="ai-mitigation-summary">
<span class="icon is-small"><i class="fa-solid fa-shield-heart"></i></span>
<span>
Mitigation Protocol
{% if latest_plan %}
· {{ latest_plan.status|title }}
{% else %}
· Not Created
{% endif %}
</span>
</summary>
<div class="ai-mitigation-collapsible-body">
{% 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" %}
{% else %}
<div id="mitigation-shell-{{ person.id }}" class="box" style="padding: 0.65rem; margin-top: 0.2rem; border: 1px dashed rgba(0, 0, 0, 0.25); box-shadow: none;">
<p class="is-size-7 has-text-grey">No mitigation plan yet. Use the Patterns tab to generate one.</p>
</div>
{% endif %}
</div>
{% endif %}
</details>
{% elif operation == "draft_reply" and draft_replies %}
<div id="draft-host-{{ person.id }}-{{ operation }}" data-selected="0">
<div class="columns is-multiline" style="margin: 0 -0.35rem;">
@@ -177,31 +198,58 @@
{% 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;">
<article
id="ai-signals-{{ person.id }}-{{ operation }}"
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>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.35rem;">
Click a signal to highlight related memory proposals and evidence citations.
</p>
<div class="tags">
{% for signal in interaction_signals %}
<span class="tag is-light">{{ signal.label }} ({{ signal.valence }})</span>
<button
type="button"
class="tag is-light js-ai-signal-tag ai-linkable"
data-signal-key="{{ signal.signal_key|default:signal.label }}"
data-citation-ids="{{ signal.message_event_ids|join:',' }}"
title="{{ signal.meaning|default:'Linked conversational signal from this AI run.' }}">
{{ signal.display_label|default:signal.label|title }} ({{ signal.valence|title }})
</button>
{% endfor %}
</div>
</article>
{% endif %}
{% if memory_proposals %}
<section style="margin-top: 0.55rem;">
<section id="ai-memory-proposals-{{ person.id }}-{{ operation }}" style="margin-top: 0.55rem;">
<p class="is-size-7 has-text-weight-semibold" style="margin-bottom: 0.35rem;">Memory Proposals</p>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.35rem;">
Candidate memory extracts grouped by theme. Click a group to reveal linked signals and citations.
</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;">
<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>
<div
class="js-ai-memory-group ai-linkable"
tabindex="0"
role="button"
data-memory-key="{{ group.key|default:group.title }}"
data-signal-keys="{{ group.signal_keys|join:',' }}"
title="Highlight related interaction signals and citations."
style="padding: 0.1rem 0.15rem; border-radius: 8px;">
<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>
</div>
</div>
{% endfor %}
</div>
@@ -216,12 +264,18 @@
{% 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;">
<article
id="ai-citations-{{ person.id }}-{{ operation }}"
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>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.35rem;">
These are source messages used as evidence for this AI result.
</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;">
<p class="is-size-7 js-ai-citation-row" data-citation-id="{{ row.id }}" 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>
@@ -283,16 +337,231 @@
</form>
</article>
<div id="mitigation-shell-{{ person.id }}" class="box" style="padding: 0.65rem; margin-top: 0.65rem; border: 1px dashed rgba(0, 0, 0, 0.25); box-shadow: none;">
<div class="box" style="padding: 0.65rem; margin-top: 0.65rem; border: 1px dashed rgba(0, 0, 0, 0.25); box-shadow: none;">
<p class="is-size-7 has-text-grey">
Plan editing is consolidated in the <strong>Plan</strong> tab.
</p>
<div class="buttons are-small" style="margin-top: 0.4rem; margin-bottom: 0;">
<button
type="button"
class="button is-small is-light"
onclick="giaWorkspaceOpenTab('{{ person.id }}', 'plan_board', false); return false;">
<span class="icon is-small"><i class="fa-solid fa-shield-heart"></i></span>
<span>Open Plan Tab</span>
</button>
</div>
</div>
{% endif %}
{% endif %}
</div>
<script>
(function () {
const root = document.getElementById(
"ai-result-links-{{ person.id }}-{{ operation }}-{{ ai_result_id|default:'new' }}"
);
if (!root || root.dataset.linkedBound === "1") {
return;
}
root.dataset.linkedBound = "1";
const signalTags = Array.from(root.querySelectorAll(".js-ai-signal-tag"));
const memoryGroups = Array.from(root.querySelectorAll(".js-ai-memory-group"));
const citationRows = Array.from(root.querySelectorAll(".js-ai-citation-row"));
const citationsBox = root.querySelector(
"#ai-citations-{{ person.id }}-{{ operation }}"
);
const memorySection = root.querySelector(
"#ai-memory-proposals-{{ person.id }}-{{ operation }}"
);
const signalsSection = root.querySelector(
"#ai-signals-{{ person.id }}-{{ operation }}"
);
const citationById = new Map();
citationRows.forEach(function (row) {
const key = String(row.dataset.citationId || "").trim();
if (key) {
citationById.set(key, row);
}
});
const normalize = function (value) {
return String(value || "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.replace("open_loops", "open_loop");
};
const listFromCsv = function (value) {
return String(value || "")
.split(",")
.map(function (item) {
return item.trim();
})
.filter(Boolean);
};
const flashNode = function (node) {
if (!node) {
return;
}
node.classList.remove("ai-link-flash");
void node.offsetWidth;
node.classList.add("ai-link-flash");
window.setTimeout(function () {
node.classList.remove("ai-link-flash");
}, 900);
};
const flashCitationsByIds = function (ids) {
let matched = false;
ids.forEach(function (id) {
const node = citationById.get(String(id || "").trim());
if (node) {
matched = true;
flashNode(node);
}
});
if (matched) {
flashNode(citationsBox);
} else {
flashNode(citationsBox);
}
};
const signalKeyFromNode = function (node) {
return normalize(node.dataset.signalKey || node.textContent || "");
};
const handleSignalClick = function (sourceTag) {
const key = signalKeyFromNode(sourceTag);
const ids = listFromCsv(sourceTag.dataset.citationIds || "");
signalTags.forEach(function (tag) {
if (signalKeyFromNode(tag) === key) {
flashNode(tag);
}
});
let matchedGroup = false;
memoryGroups.forEach(function (group) {
const memoryKey = normalize(group.dataset.memoryKey || "");
const signalKeys = listFromCsv(group.dataset.signalKeys || "").map(normalize);
const linked = signalKeys.includes(key) || memoryKey === key;
if (linked) {
matchedGroup = true;
flashNode(group);
}
});
if (!matchedGroup) {
flashNode(memorySection);
}
flashCitationsByIds(ids);
};
const handleMemoryClick = function (group) {
const signalKeys = listFromCsv(group.dataset.signalKeys || "").map(normalize);
const memoryKey = normalize(group.dataset.memoryKey || "");
const allKeys = Array.from(new Set(signalKeys.concat([memoryKey])));
flashNode(group);
let matchedSignal = false;
const ids = [];
signalTags.forEach(function (tag) {
const key = signalKeyFromNode(tag);
if (allKeys.includes(key)) {
matchedSignal = true;
flashNode(tag);
ids.push.apply(ids, listFromCsv(tag.dataset.citationIds || ""));
}
});
if (!matchedSignal) {
flashNode(signalsSection);
}
flashCitationsByIds(ids);
};
signalTags.forEach(function (tag) {
tag.addEventListener("click", function () {
handleSignalClick(tag);
});
});
memoryGroups.forEach(function (group) {
group.addEventListener("click", function () {
handleMemoryClick(group);
});
group.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleMemoryClick(group);
}
});
});
})();
</script>
<style>
.ai-mitigation-collapsible {
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 8px;
background: rgba(255, 255, 255, 0.75);
padding: 0.3rem 0.45rem;
}
.ai-mitigation-summary {
list-style: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.32rem;
font-size: 0.76rem;
font-weight: 600;
color: #35475f;
}
.ai-mitigation-summary::-webkit-details-marker {
display: none;
}
.ai-mitigation-collapsible .ai-mitigation-collapsible-body {
margin-top: 0.35rem;
}
.js-ai-signal-tag {
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.16);
background: #fff;
}
.ai-linkable {
cursor: pointer;
transition: background-color 120ms ease, box-shadow 120ms ease;
}
.ai-linkable:hover {
background: rgba(54, 114, 206, 0.06);
}
.ai-linkable:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(50, 115, 220, 0.25);
}
.ai-link-flash {
animation: aiLinkFlash 0.9s ease-out;
}
@keyframes aiLinkFlash {
0% {
background-color: rgba(50, 115, 220, 0.12);
box-shadow: 0 0 0 0 rgba(50, 115, 220, 0.24);
}
40% {
background-color: rgba(50, 115, 220, 0.2);
box-shadow: 0 0 0 2px rgba(50, 115, 220, 0.2);
}
100% {
background-color: transparent;
box-shadow: 0 0 0 0 rgba(50, 115, 220, 0);
}
}
.draft-option-card.is-selected {
border-color: rgba(54, 54, 54, 0.85) !important;
border-width: 2px !important;