diff --git a/CHANGELOG/pages/proto-home-v5.md b/CHANGELOG/pages/proto-home-v5.md index eac43bf5..1d69fc4b 100644 --- a/CHANGELOG/pages/proto-home-v5.md +++ b/CHANGELOG/pages/proto-home-v5.md @@ -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 diff --git a/public/proto/home-v5.html b/public/proto/home-v5.html index 54a003a4..a562571d 100644 --- a/public/proto/home-v5.html +++ b/public/proto/home-v5.html @@ -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; } @@ -3289,6 +3303,15 @@

Your wiki is live.

+ +
+ + 0 in the room. The more people here, the richer this room’s wiki — invite a few. + + +
+

AI Infra Summit

Disposable event brain · 0 joined · public wiki later

@@ -4629,6 +4652,40 @@

Keyboard shortcuts

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: 'This is your input. Type to chat with the room, or start with /ask to get a sourced agent answer.' }, @@ -7426,6 +7483,7 @@

Keyboard shortcuts

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. diff --git a/tests/e2e/scratchnode-live-route-honesty.spec.ts b/tests/e2e/scratchnode-live-route-honesty.spec.ts index bc2a7398..0d7d061d 100644 --- a/tests/e2e/scratchnode-live-route-honesty.spec.ts +++ b/tests/e2e/scratchnode-live-route-honesty.spec.ts @@ -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"); + }); });