2ab8fcbdf0
The drawer's Significance review panel previously only supported per-memory edits. Adds a bulk control: pick ``level_from`` and ``level_to``, and every memory in the chat at ``level_from`` is moved to ``level_to``. Implementation emits one ``manual_edit`` event per matching memory (not a single bulk event) so the §6.4 per-row audit trail stays intact — each affected memory carries its own ``prior_value -> new_value`` snapshot, so an inverse edit can restore an individual row without needing to inspect a bulk payload's member list. Reuses the existing ``memory_significance`` ``manual_edit`` projector branch (T25), so no state-layer changes are required. The route rejects no-op submissions (``level_from == level_to``) with 400 to avoid padding the event log with empty edits, and clamps both levels to 0..3 (matching ``edit_memory_significance``). UI: a small ``<details>`` block in the Significance review section with two number inputs and a submit button. Test: tests/test_drawer_phase4.py::test_bulk_significance_re_rate_emits_manual_edit_per_memory.
654 lines
25 KiB
HTML
654 lines
25 KiB
HTML
<div class="drawer-content">
|
|
<header class="drawer-header">
|
|
<h2>{{ host_bot.name }}</h2>
|
|
<button class="drawer-close" type="button"
|
|
onclick="document.getElementById('drawer').setAttribute('hidden','')">×</button>
|
|
</header>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Scene</h3>
|
|
{% if scene %}
|
|
<p>Started: {{ scene.started_at }}</p>
|
|
{% endif %}
|
|
{% if container %}
|
|
<p>Container: {{ container.name }} ({{ container.type }})</p>
|
|
{% else %}
|
|
<p class="muted">No active container.</p>
|
|
{% endif %}
|
|
<p>Time: {{ chat.time }}</p>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/chat/narrative-anchor"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Narrative anchor:
|
|
<input type="text" name="new_value" maxlength="500"
|
|
value="{{ chat.narrative_anchor or '' }}">
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/chat/weather"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Weather:
|
|
<input type="text" name="new_value" maxlength="500"
|
|
value="{{ chat.weather or '' }}">
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
{% if scene %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/scene/close"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<button type="submit">Close scene</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="muted">No active scene.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Activity</h3>
|
|
{% for label, act in [("you", you_activity), (host_bot.name, bot_activity)] %}
|
|
<div class="activity-row">
|
|
<strong>{{ label }}</strong>
|
|
{% if act %}
|
|
<p>{{ act.posture or "—" }} / {{ (act.action or {}).verb or "—" }}</p>
|
|
{% if act.attention %}<p class="muted">attention: {{ act.attention }}</p>{% endif %}
|
|
{% if act.holding %}<p class="muted">holding: {{ act.holding|join(", ") }}</p>{% endif %}
|
|
{% else %}
|
|
<p class="muted">No activity recorded.</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<details class="skip-controls">
|
|
<summary>Elision skip</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/skip/elision"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Landing state hint:
|
|
<input type="text" name="landing_state_hint"
|
|
placeholder="e.g. arriving at the office">
|
|
</label>
|
|
<label>
|
|
New time (ISO 8601):
|
|
<input type="text" name="new_time" required
|
|
placeholder="2026-04-26T20:30:00+00:00">
|
|
</label>
|
|
<button type="submit">Skip ahead</button>
|
|
</form>
|
|
</details>
|
|
|
|
<details class="skip-controls">
|
|
<summary>Jump skip</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/skip/jump"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
New time (ISO 8601):
|
|
<input type="text" name="new_time" required
|
|
placeholder="2026-04-27T08:00:00+00:00">
|
|
</label>
|
|
<label>
|
|
Anything notable happen? (optional)
|
|
<textarea name="notable_prose" rows="3"
|
|
placeholder="leave blank to jump without synthesizing memories"></textarea>
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" name="reset_activity" value="1">
|
|
Reset activity at landing
|
|
</label>
|
|
<button type="submit">Jump ahead</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Events</h3>
|
|
{% if active_events %}
|
|
<ul class="event-list">
|
|
{% for ev in active_events %}
|
|
<li class="event-row">
|
|
<strong>{{ ev.kind }}</strong>
|
|
<span class="muted"> ({{ ev.status }})</span>
|
|
{% if ev.planned_for %}
|
|
<p class="muted">planned for: {{ ev.planned_for }}</p>
|
|
{% endif %}
|
|
{% if ev.props %}
|
|
<p class="muted">{{ ev.props|tojson }}</p>
|
|
{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/event/cancel/{{ ev.event_id }}"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<button type="submit">Cancel</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No active events.</p>
|
|
{% endif %}
|
|
<details>
|
|
<summary>Plan event</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/event/plan"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Kind:
|
|
<input type="text" name="kind" required
|
|
placeholder="e.g. dinner_reservation">
|
|
</label>
|
|
<label>
|
|
Planned for (ISO 8601):
|
|
<input type="text" name="planned_for" required
|
|
placeholder="2026-04-26T19:00:00+00:00">
|
|
</label>
|
|
<label>
|
|
Props (JSON):
|
|
<textarea name="props_json" rows="3"
|
|
placeholder='{"location": "Bistro X"}'>{}</textarea>
|
|
</label>
|
|
<button type="submit">Plan event</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Threads</h3>
|
|
{% if open_threads %}
|
|
<ul class="thread-list">
|
|
{% for th in open_threads %}
|
|
<li class="thread-row">
|
|
<strong>{{ th.title }}</strong>
|
|
{% if th.summary %}
|
|
<p>{{ th.summary }}</p>
|
|
{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/thread/close/{{ th.thread_id }}"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<button type="submit">Close</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No open threads.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% if guest_bot %}
|
|
<section class="drawer-section">
|
|
<h3>Guest</h3>
|
|
<p><strong>{{ guest_bot.name }}</strong></p>
|
|
{% if guest_activity %}
|
|
<p>{{ guest_activity.posture or "—" }} / {{ (guest_activity.action or {}).verb or "—" }}</p>
|
|
{% if guest_activity.attention %}<p class="muted">attention: {{ guest_activity.attention }}</p>{% endif %}
|
|
{% if guest_activity.holding %}<p class="muted">holding: {{ guest_activity.holding|join(", ") }}</p>{% endif %}
|
|
{% else %}
|
|
<p class="muted">No activity recorded.</p>
|
|
{% endif %}
|
|
|
|
{% if edge_h2g %}
|
|
<div class="edge-row">
|
|
<strong>{{ host_bot.name }} → {{ guest_bot.name }}</strong>
|
|
<p>Affinity: {{ edge_h2g.affinity }}/100 · Trust: {{ edge_h2g.trust }}/100</p>
|
|
{% if edge_h2g.knowledge %}
|
|
<details><summary>Knowledge ({{ edge_h2g.knowledge|length }})</summary>
|
|
<ul>{% for fact in edge_h2g.knowledge %}<li>{{ fact }}</li>{% endfor %}</ul>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% if edge_g2h %}
|
|
<div class="edge-row">
|
|
<strong>{{ guest_bot.name }} → {{ host_bot.name }}</strong>
|
|
<p>Affinity: {{ edge_g2h.affinity }}/100 · Trust: {{ edge_g2h.trust }}/100</p>
|
|
{% if edge_g2h.knowledge %}
|
|
<details><summary>Knowledge ({{ edge_g2h.knowledge|length }})</summary>
|
|
<ul>{% for fact in edge_g2h.knowledge %}<li>{{ fact }}</li>{% endfor %}</ul>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% if edge_y2g %}
|
|
<div class="edge-row">
|
|
<strong>you → {{ guest_bot.name }}</strong>
|
|
<p>Affinity: {{ edge_y2g.affinity }}/100 · Trust: {{ edge_y2g.trust }}/100</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if edge_g2y %}
|
|
<div class="edge-row">
|
|
<strong>{{ guest_bot.name }} → you</strong>
|
|
<p>Affinity: {{ edge_g2y.affinity }}/100 · Trust: {{ edge_g2y.trust }}/100</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/guest/remove"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<button type="submit">Remove guest</button>
|
|
</form>
|
|
</section>
|
|
{% else %}
|
|
<section class="drawer-section">
|
|
<h3>Add guest</h3>
|
|
{% if available_guests %}
|
|
{% set first_guest_id = available_guests[0].id %}
|
|
{% set first_existing = existing_guest_edges.get(first_guest_id, False) %}
|
|
<form class="inline-edit add-guest-form"
|
|
hx-post="/chats/{{ chat.id }}/drawer/guest/add"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Bot:
|
|
<select name="guest_bot_id" required class="add-guest-select">
|
|
{% for b in available_guests %}
|
|
<option value="{{ b.id }}"
|
|
data-existing-edge="{{ 'true' if existing_guest_edges.get(b.id) else 'false' }}">
|
|
{{ b.name }}{% if existing_guest_edges.get(b.id) %} (already met){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<p class="muted add-guest-existing-note"
|
|
{% if not first_existing %}hidden{% endif %}>
|
|
they already know each other (edge exists from a prior chat)
|
|
</p>
|
|
<label class="add-guest-reseed-label"
|
|
{% if not first_existing %}hidden{% endif %}>
|
|
<input type="checkbox" name="reseed" value="1" class="add-guest-reseed">
|
|
re-seed anyway
|
|
</label>
|
|
<label>
|
|
Have they met before? Describe how (leave blank if not):
|
|
<textarea name="relationship_prose" rows="3"
|
|
class="add-guest-prose"
|
|
{% if first_existing %}disabled{% endif %}
|
|
placeholder="e.g. Old college friends who studied physics together."></textarea>
|
|
</label>
|
|
<button type="submit">Add guest</button>
|
|
</form>
|
|
<script>
|
|
(function () {
|
|
var form = document.currentScript.previousElementSibling;
|
|
while (form && !form.classList.contains('add-guest-form')) {
|
|
form = form.previousElementSibling;
|
|
}
|
|
if (!form) return;
|
|
var sel = form.querySelector('.add-guest-select');
|
|
var prose = form.querySelector('.add-guest-prose');
|
|
var reseed = form.querySelector('.add-guest-reseed');
|
|
var note = form.querySelector('.add-guest-existing-note');
|
|
var reseedLabel = form.querySelector('.add-guest-reseed-label');
|
|
function refresh() {
|
|
var opt = sel.options[sel.selectedIndex];
|
|
var existing = opt && opt.getAttribute('data-existing-edge') === 'true';
|
|
if (existing) {
|
|
note.removeAttribute('hidden');
|
|
reseedLabel.removeAttribute('hidden');
|
|
prose.disabled = !reseed.checked;
|
|
} else {
|
|
note.setAttribute('hidden', '');
|
|
reseedLabel.setAttribute('hidden', '');
|
|
reseed.checked = false;
|
|
prose.disabled = false;
|
|
}
|
|
}
|
|
sel.addEventListener('change', refresh);
|
|
reseed.addEventListener('change', refresh);
|
|
refresh();
|
|
})();
|
|
</script>
|
|
{% else %}
|
|
<p class="muted">No other bots authored yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if group_node %}
|
|
<section class="drawer-section">
|
|
<h3>Group</h3>
|
|
{% if group_node.summary %}
|
|
<p>{{ group_node.summary }}</p>
|
|
{% else %}
|
|
<p class="muted">No group summary yet.</p>
|
|
{% endif %}
|
|
{% if group_node.dynamic %}
|
|
<p class="muted">Dynamic: {{ group_node.dynamic }}</p>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="drawer-section">
|
|
<h3>Edges</h3>
|
|
{% if edge_b2y %}
|
|
<div class="edge-row">
|
|
<strong>{{ host_bot.name }} → you</strong>
|
|
<p>Affinity: {{ edge_b2y.affinity }}/100 · Trust: {{ edge_b2y.trust }}/100</p>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/{{ host_bot.id }}/you/affinity"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Affinity:
|
|
<input type="range" name="affinity" min="0" max="100"
|
|
value="{{ edge_b2y.affinity }}"
|
|
oninput="this.nextElementSibling.value = this.value">
|
|
<output>{{ edge_b2y.affinity }}</output>
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/trust"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="{{ host_bot.id }}">
|
|
<input type="hidden" name="target_id" value="you">
|
|
<label>
|
|
Trust:
|
|
<input type="range" name="new_value" min="0" max="100"
|
|
value="{{ edge_b2y.trust }}"
|
|
oninput="this.nextElementSibling.value = this.value">
|
|
<output>{{ edge_b2y.trust }}</output>
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/summary"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="{{ host_bot.id }}">
|
|
<input type="hidden" name="target_id" value="you">
|
|
<label>
|
|
Summary:
|
|
<textarea name="new_summary" rows="3" maxlength="2000">{{ edge_b2y.summary or "" }}</textarea>
|
|
</label>
|
|
<button type="submit">Save summary</button>
|
|
</form>
|
|
<details>
|
|
<summary>Knowledge ({{ (edge_b2y.knowledge or [])|length }})</summary>
|
|
{% if edge_b2y.knowledge %}
|
|
<ul>
|
|
{% for fact in edge_b2y.knowledge %}
|
|
<li>
|
|
{{ fact }}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/knowledge-facts"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="{{ host_bot.id }}">
|
|
<input type="hidden" name="target_id" value="you">
|
|
<input type="hidden" name="action" value="remove">
|
|
<input type="hidden" name="fact" value="{{ fact }}">
|
|
<button type="submit">Remove</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/knowledge-facts"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="{{ host_bot.id }}">
|
|
<input type="hidden" name="target_id" value="you">
|
|
<input type="hidden" name="action" value="add">
|
|
<label>
|
|
Add fact:
|
|
<input type="text" name="fact" maxlength="500" required>
|
|
</label>
|
|
<button type="submit">Add</button>
|
|
</form>
|
|
</details>
|
|
</div>
|
|
{% endif %}
|
|
{% if edge_y2b %}
|
|
<div class="edge-row">
|
|
<strong>you → {{ host_bot.name }}</strong>
|
|
<p>Affinity: {{ edge_y2b.affinity }}/100 · Trust: {{ edge_y2b.trust }}/100</p>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/trust"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="you">
|
|
<input type="hidden" name="target_id" value="{{ host_bot.id }}">
|
|
<label>
|
|
Trust:
|
|
<input type="range" name="new_value" min="0" max="100"
|
|
value="{{ edge_y2b.trust }}"
|
|
oninput="this.nextElementSibling.value = this.value">
|
|
<output>{{ edge_y2b.trust }}</output>
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/edge/summary"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_id" value="you">
|
|
<input type="hidden" name="target_id" value="{{ host_bot.id }}">
|
|
<label>
|
|
Summary:
|
|
<textarea name="new_summary" rows="3" maxlength="2000">{{ edge_y2b.summary or "" }}</textarea>
|
|
</label>
|
|
<button type="submit">Save summary</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% if not edge_b2y and not edge_y2b %}
|
|
<p class="muted">No edges yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Branches</h3>
|
|
{% if branches %}
|
|
<ul class="branch-list">
|
|
{% for b in branches %}
|
|
<li class="branch-row{% if b.is_active %} branch-active{% endif %}">
|
|
<strong>{{ b.name }}</strong>
|
|
{% if b.is_active %}<span class="muted"> (active)</span>{% endif %}
|
|
<span class="muted"> · {{ b.event_count }} events</span>
|
|
{% if not b.is_active %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/branch/switch"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="name" value="{{ b.name }}">
|
|
<button type="submit">Switch</button>
|
|
</form>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No branches yet.</p>
|
|
{% endif %}
|
|
<details>
|
|
<summary>Create branch</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/branch/create"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Name:
|
|
<input type="text" name="name" required
|
|
placeholder="e.g. experiment_a">
|
|
</label>
|
|
<label>
|
|
Origin event id:
|
|
<input type="number" name="origin_event_id" required min="0">
|
|
</label>
|
|
<button type="submit">Create</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Recent turns</h3>
|
|
{% if recent_turns %}
|
|
<ul class="recent-turns-list">
|
|
{% for t in recent_turns %}
|
|
<li class="turn-row{% if t.hidden %} turn-hidden{% endif %}">
|
|
<span class="muted">#{{ t.event_id }} {{ t.kind }}</span>
|
|
<strong>{{ t.speaker }}:</strong>
|
|
{{ t.excerpt }}{% if t.excerpt|length >= 120 %}…{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/turn/hide/{{ t.event_id }}"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="hidden" value="{{ 0 if t.hidden else 1 }}">
|
|
<label>
|
|
<input type="checkbox" {% if t.hidden %}checked{% endif %}
|
|
onchange="this.form.requestSubmit()">
|
|
hide from view
|
|
</label>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No turns yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Significance review</h3>
|
|
{% set total_mem = significance_distribution.values()|sum %}
|
|
{% if total_mem %}
|
|
<ul class="significance-distribution">
|
|
{% for level in [0, 1, 2, 3] %}
|
|
{% set count = significance_distribution[level] %}
|
|
{% set marker = ['·','•','★','★★'][level] %}
|
|
{% set pct = (100 * count / total_mem)|round(0, 'floor')|int if total_mem else 0 %}
|
|
<li class="sig-bar sig-{{ level }}">
|
|
<span class="sig-label">{{ marker }} ({{ level }})</span>
|
|
<span class="sig-bar-fill" style="width: {{ pct }}%"></span>
|
|
<span class="sig-count">{{ count }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No memories yet.</p>
|
|
{% endif %}
|
|
{% if recent_memories %}
|
|
<details>
|
|
<summary>Edit significance (recent memories)</summary>
|
|
<ul class="significance-edit-list">
|
|
{% for m in recent_memories %}
|
|
<li>
|
|
<span class="sig sig-{{ m.significance }}">{{ ['·','•','★','★★'][m.significance|default(0)] }}</span>
|
|
{{ m.pov_summary[:80] }}{% if m.pov_summary|length > 80 %}…{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/{{ m.id }}/significance"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
Significance:
|
|
<input type="range" name="significance" min="0" max="3"
|
|
value="{{ m.significance|default(0) }}"
|
|
oninput="this.nextElementSibling.value = this.value">
|
|
<output>{{ m.significance|default(0) }}</output>
|
|
</label>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
{% endif %}
|
|
{# T110.4: bulk significance re-rate. Move every memory in this chat
|
|
at level_from to level_to with one manual_edit event per row, so
|
|
the audit trail stays per-memory. #}
|
|
<details class="bulk-significance">
|
|
<summary>Bulk re-rate significance</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/significance/bulk"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<label>
|
|
From:
|
|
<input type="number" name="level_from" min="0" max="3" value="0" required>
|
|
</label>
|
|
<label>
|
|
To:
|
|
<input type="number" name="level_to" min="0" max="3" value="1" required>
|
|
</label>
|
|
<button type="submit">Re-rate all</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Pinned memories ({{ pinned|length }} / {{ pin_cap }})</h3>
|
|
{% if pinned %}
|
|
<ul class="memory-list">
|
|
{% for m in pinned %}
|
|
<li>
|
|
<span class="sig sig-{{ m.significance }}">{{ ['·','•','★','★★'][m.significance|default(0)] }}</span>
|
|
{{ m.pov_summary }}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/{{ m.id }}/pin"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="pinned" value="0">
|
|
<button type="submit">Unpin</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No pinned memories.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<h3>Recent memories</h3>
|
|
{% if recent_memories %}
|
|
<ul class="memory-list">
|
|
{% for m in recent_memories %}
|
|
<li>
|
|
<span class="sig sig-{{ m.significance }}">{{ ['·','•','★','★★'][m.significance|default(0)] }}</span>
|
|
{{ m.pov_summary[:200] }}{% if m.pov_summary|length > 200 %}…{% endif %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/{{ m.id }}/significance"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<select name="significance">
|
|
{% for s in [0, 1, 2, 3] %}
|
|
<option value="{{ s }}" {% if m.significance == s %}selected{% endif %}>
|
|
{{ ['·','•','★','★★'][s] }} ({{ s }})
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit">Set</button>
|
|
</form>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/{{ m.id }}/pin"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="pinned" value="{{ 0 if m.pinned else 1 }}">
|
|
<button type="submit">{{ 'Unpin' if m.pinned else 'Pin' }}</button>
|
|
</form>
|
|
<div class="witness-row">
|
|
{% for flag in ['you', 'host', 'guest'] %}
|
|
{% set witnessed = m['witness_' ~ flag] %}
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/witness"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="memory_id" value="{{ m.id }}">
|
|
<input type="hidden" name="flag" value="{{ flag }}">
|
|
<input type="hidden" name="new_value" value="{{ 0 if witnessed else 1 }}">
|
|
<label>
|
|
<input type="checkbox" {% if witnessed %}checked{% endif %}
|
|
onchange="this.form.requestSubmit()">
|
|
{{ flag }}
|
|
</label>
|
|
</form>
|
|
{% endfor %}
|
|
</div>
|
|
<details>
|
|
<summary>Edit POV summary</summary>
|
|
<form class="inline-edit"
|
|
hx-post="/chats/{{ chat.id }}/drawer/memory/pov-summary"
|
|
hx-target="#drawer" hx-swap="innerHTML">
|
|
<input type="hidden" name="memory_id" value="{{ m.id }}">
|
|
<textarea name="new_summary" rows="3" maxlength="2000">{{ m.pov_summary }}</textarea>
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
</details>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">No memories yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|