havre: hear / whisper on the same line as remember / remind / delete (and fresh replies get that line too)
ci / test (pull_request) Successful in 4s

This commit is contained in:
teahubxyz committed 2026-10-21 19:51:13 +00:00
1 parent 2e86c329d4
commit 4a146969c4
1 file changed
+9 -7
+9 -7
View File
@@ -1028,11 +1028,13 @@ function jumpTo(t, m) {
}
// ---------- editing and deleting messages ----------
function msgTools(el, t, m) {
function msgTools(el, t, m, voice = []) {
if (!t || !m || !m.id) return;
el.dataset.msg = m.id;
for (const old of $$(".msgtools", el)) old.remove();
const c = cardOf(t), words = !!String(m.content || "").trim() && c && !c.plain && S.ab.memory;
el.append(h("span", { class: "msgtools" }, ...slashed([
...voice,
m.role === "user" && !m.media ? linkish("edit", () => editMsg(t, m), { "aria-label": "Edit this message" }) : null,
words ? linkish("remember", () => rememberMsg(t, m), { "aria-label": "Add this to what they remember", title: "into their memory, in these words" }) : null,
words ? linkish("remind", () => remindMsg(t, m), { "aria-label": "Remind them of this at a time", title: "back to mind at a time you pick" }) : null,
@@ -1122,7 +1124,7 @@ function renderLog() {
if (m.voice) put(voiceLine(c, m));
let last = null;
for (const b of bubblesOf(c, m.content)) { last = put(line(b, "them")); if (m.id) last.dataset.msg = m.id; }
if (last) { offerVoice(last, c, m, t); msgTools(last, t, m); }
if (last) msgTools(last, t, m, offerVoice(last, c, m, t));
}
}
const ideas = hist.some((m) => m.role === "user" && !m.hidden) ? [] : C.suggestions(c);
@@ -1376,7 +1378,7 @@ async function play(t, bubbles, signal, c, msg, drawn = S.gen) {
if (msg && msg.id) last.dataset.msg = msg.id;
scrollLog();
}
if (last && c && msg) offerVoice(last, c, msg, t);
if (last && c && msg) msgTools(last, t, msg, offerVoice(last, c, msg, t));
}
// ---------- photos, clips, voice notes, and yours, in the conversation ----------
@@ -1446,7 +1448,7 @@ function voiceLine(c, m) {
/** "hear it" after a reply that came as text: its voice note, made when asked for (once, however often it's clicked) */
const voicing = new WeakSet();
function offerVoice(el, c, m, t) {
if (m.voice || m.demo || c.plain || !S.key || !t) return;
if (m.voice || m.demo || c.plain || !S.key || !t) return [];
const btns = [];
const make = (label, asmr) => {
const b = linkish(voicing.has(m) ? "recording…" : label, async () => {
@@ -1457,15 +1459,15 @@ function offerVoice(el, c, m, t) {
try {
m.voice = await makeVoice(c, m.content, undefined, { asmr });
saveThread(t);
if (b.isConnected) { b.replaceWith(audioEl(m.voice.id)); for (const x of btns) if (x !== b) x.remove(); }
else placeVoice(t, m);
placeVoice(t, m); // the voice note goes above the words
if (el.isConnected) msgTools(el, t, m); // and the line under them loses hear / whisper
balanceSoon();
} catch (e) { for (const x of btns) x.disabled = false; b.textContent = label; toast(e.message); } finally { voicing.delete(m); }
}, { class: "linkish hear", "aria-label": asmr ? `Hear this whispered by ${c.name}, over soft rain` : `Hear this as a voice note from ${c.name}`, disabled: voicing.has(m) || null });
btns.push(b);
return b;
};
$(".said", el).append(" ", make("hear it", false), " ", make("whisper it", true));
return [make("hear", false), make("whisper", true)];
}
/** a voice note of these words, in the character's voice and mood */
async function makeVoice(c, text, signal, { asmr = false } = {}) {