607d0971c4
Memories grow per-flag witness checkboxes (you / host / guest) that
auto-submit on change via HTMX. The new POST route emits a manual_edit
event with target_kind=memory_witness and a {flag, value} payload;
prior_value mirrors the same shape so an inverse edit restores the
flag. The drawer's recent-memories query now selects the three
witness columns alongside the existing fields so the template can
render checkbox state without a second query per row.
385 lines
16 KiB
HTML
385 lines
16 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>
|
|
{% 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 %}
|
|
</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>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>
|