Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG/pages/proto-home-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

Append-only lane for the ScratchNode live-event prototype and production static surface.

## 2026-06-03 — In-room "invite more → richer wiki" memory nudge
The viral framing ("the room becomes the memory") only appeared at create-time and in
the publish recap — never *during* the live event, where the audit flagged it absent.
Added a small, dismissible in-room nudge that reveals once (after a 4s beat so it doesn't
crowd the join moment) reinforcing the memory framing and offering a one-tap **Invite**
(opens the existing in-room share sheet).

HONESTY: the count is the **real** `events:getMembers` live member count — the nudge
never renders until there is real presence (≥1), and the figure read at reveal time is
the freshest real `_sn_live_members.length`, never fabricated. Dismiss is sticky
(`localStorage sn_mem_nudge_off`), it shows at most once per load, and it's motion-safe
(subtle fade gated under `prefers-reduced-motion`, terracotta glass DNA).

Covered by a new honesty case (reveals with the real count `2`, Invite visible, sticky
dismiss flips `data-show=false` + sets the flag). 26/26 chromium honesty + output-contract
green; screenshot-verified.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-03 — Honesty: stop the 4 "Continue in NodeBench" CTAs from 404'ing
A scoping audit caught a live HONEST_STATUS lie: **four** in-room CTAs ("Continue in
NodeBench", "Open in NodeBench", "Open NodeBench event notebook", "Continue this event
Expand Down
58 changes: 58 additions & 0 deletions public/proto/home-v5.html
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,20 @@
.welcome-close { background: none; border: 0; color: var(--ink-faint); font-size: 18px; line-height: 1; padding: 4px; min-width: 32px; }
.welcome-close:hover { color: var(--ink); }

/* In-room memory nudge — reinforces "the room becomes the wiki, invite more"
using the REAL live presence count. Shown once per browser, dismissible. */
.sn-mem-nudge { display: none; align-items: center; gap: 10px; margin: 0 0 10px; padding: 9px 12px; border: 1px solid rgba(217,119,87,.28); border-radius: 8px; background: rgba(217,119,87,.07); font-size: 13px; color: var(--ink-muted); }
.sn-mem-nudge[data-show="true"] { display: flex; animation: snMemNudgeIn var(--sn-dur-slow, .4s) var(--sn-ease-out, ease) both; }
.sn-mem-nudge__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green, #5ea867); flex-shrink: 0; }
.sn-mem-nudge__text { flex: 1; line-height: 1.4; }
.sn-mem-nudge__text b { color: var(--ink); }
.sn-mem-nudge__invite { flex-shrink: 0; font-family: var(--ui); font-size: 12px; font-weight: 600; padding: 5px 11px; border-radius: 7px; cursor: pointer; border: 1px solid rgba(217,119,87,.4); background: rgba(217,119,87,.12); color: var(--accent); }
.sn-mem-nudge__invite:hover { background: var(--accent); color: #fff; }
.sn-mem-nudge__close { flex-shrink: 0; background: none; border: 0; color: var(--ink-faint); font-size: 16px; line-height: 1; padding: 4px; min-width: 28px; cursor: pointer; }
.sn-mem-nudge__close:hover { color: var(--ink); }
@keyframes snMemNudgeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .sn-mem-nudge[data-show="true"] { animation: none; } }

/* ─── Identity row ─── */
.id-row { display: flex; align-items: center; gap: 8px; padding: 6px 0 14px; font-size: 12px; color: var(--ink-faint); }
.id-avatar { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), #b85f44); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
Expand Down Expand Up @@ -3289,6 +3303,15 @@ <h2 id="wiki-live-title">Your wiki is live.</h2>
<button class="welcome-close" type="button" onclick="dismissWelcome()" aria-label="Dismiss">&times;</button>
</div>

<!-- In-room memory nudge — revealed once (real live count) to reinforce that
the room becomes the wiki and nudge a one-tap invite. -->
<div class="sn-mem-nudge" id="sn-mem-nudge" role="status" aria-live="polite">
<span class="sn-mem-nudge__dot" aria-hidden="true"></span>
<span class="sn-mem-nudge__text"><b id="sn-mem-nudge-count">0</b> in the room. The more people here, the richer this room&rsquo;s wiki &mdash; invite a few.</span>
<button type="button" class="sn-mem-nudge__invite" onclick="_snMemNudgeInvite()">Invite &rarr;</button>
<button type="button" class="sn-mem-nudge__close" onclick="_snDismissMemNudge()" aria-label="Dismiss">&times;</button>
</div>

<section class="hero">
<h1><span id="sn-event-title-hero">AI Infra Summit</span> <button type="button" class="about-link" onclick="showAbout()" aria-label="What is this?">what is this?</button></h1>
<p class="hero-meta" id="sn-event-hero-meta">Disposable event brain · <b id="sn-hero-joined-count">0</b> joined · public wiki later</p>
Expand Down Expand Up @@ -4629,6 +4652,40 @@ <h2 id="kbd-title">Keyboard shortcuts</h2>
try { localStorage.setItem('sn_v5_seen', '1'); } catch(e){}
}

// ── In-room memory nudge ────────────────────────────────────────────────────
// Reveal ONCE (after a short beat so it doesn't crowd the join moment) to
// reinforce that the room becomes the wiki + offer a one-tap invite. HONESTY:
// the count is the REAL live member count from events:getMembers — never shown
// until there is real presence (>=1), never fabricated. Dismiss is sticky.
var _snMemNudgeArmed = false;
function _snMaybeShowMemNudge(count) {
try {
if (_snMemNudgeArmed) return; // once per load
if (!(count >= 1)) return; // real presence only
if (localStorage.getItem('sn_mem_nudge_off') === '1') return; // previously dismissed
_snMemNudgeArmed = true;
var reduce = false;
try { reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) {}
setTimeout(function () {
var el = document.getElementById('sn-mem-nudge');
var cEl = document.getElementById('sn-mem-nudge-count');
// Re-read the freshest real count at reveal time (presence may have grown).
var live = (window._sn_live_members && window._sn_live_members.length) || count;
if (cEl) cEl.textContent = String(live);
if (el) el.setAttribute('data-show', 'true');
}, reduce ? 0 : 4000);
} catch (e) { /* never break the room */ }
}
function _snDismissMemNudge() {
try { localStorage.setItem('sn_mem_nudge_off', '1'); } catch (e) {}
var el = document.getElementById('sn-mem-nudge');
if (el) el.setAttribute('data-show', 'false');
}
function _snMemNudgeInvite() {
try { if (typeof openShare === 'function') openShare(); } catch (e) {}
_snDismissMemNudge();
}

// 3-step inline tour (mini overlay positioned near each target)
var TOUR = [
{ target: '#ci', text: '<b>This is your input.</b> Type to chat with the room, or start with <code style="font-family:var(--mono);color:var(--accent)">/ask</code> to get a sourced agent answer.' },
Expand Down Expand Up @@ -7426,6 +7483,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2>
if (heroCount) heroCount.textContent = count;
const menuPeopleSub = document.getElementById('menu-people-sub');
if (menuPeopleSub) menuPeopleSub.textContent = count + ' live member' + (rows.length === 1 ? '' : 's');
if (typeof _snMaybeShowMemNudge === 'function') _snMaybeShowMemNudge(rows.length);
});

// Memory Wall (Phase 8): connect the spatial lane to the live event stream.
Expand Down
21 changes: 21 additions & 0 deletions tests/e2e/scratchnode-live-route-honesty.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,25 @@ test.describe("ScratchNode live route honesty", () => {
expect(url).toContain("continuation=private-notes");
expect(url).toContain("publicArtifact=event-wiki");
});

test("in-room memory nudge reveals once with the REAL live count and dismisses stickily", async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: "reduce" }); // reveal at 0ms (skip the 4s beat)
await fulfillScratchNodePage(page);
await page.goto("https://scratchnode.live/e/orbital", { waitUntil: "domcontentloaded" });
await expect(page.locator("body")).toHaveAttribute("data-sn-live", "true");

const nudge = page.locator("#sn-mem-nudge");
await expect(nudge).toHaveAttribute("data-show", "true", { timeout: 6_000 });
// HONESTY: the count is the REAL getMembers count (2 mock members), not fabricated.
await expect(page.locator("#sn-mem-nudge-count")).toHaveText("2");
await expect(nudge).toContainText("the richer this room");
await expect(nudge.locator(".sn-mem-nudge__invite")).toBeVisible();

// Dismiss is sticky (localStorage flag) and hides the nudge.
await page.click(".sn-mem-nudge__close");
await expect(nudge).toHaveAttribute("data-show", "false");
expect(await page.evaluate(() => localStorage.getItem("sn_mem_nudge_off"))).toBe("1");
});
});
Loading