From 945d0dac2096dc7a7042815999a7cd2064d8487c Mon Sep 17 00:00:00 2001 From: Slava Zeph Date: Sun, 12 Jul 2026 19:50:32 +0000 Subject: [PATCH 01/14] Make Mind-Hack Autopsy a playable arcade lab and add attractor evidence export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Content Reaction Lab (arcade experiment 013): - New src/features/gaugegap/ContentReactionLab.jsx: paste an ad / tweet / landing page / email, run a fully client-side scan (analysisEngine + layerRouter, no /api round-trip), watch the live 3D brain react, and read four headline scores — Attention, Trust, Emotional Charge, Manipulation Risk — with delta badges against the previous run. - One-click rewrites (build trust / add real urgency / reduce manipulation) scored in-browser with before→after deltas; new additive "urgency" goal in improve/rewrite.js. Share score card reuses ShareDialog. - Registered as lab 013 in ExperimentArcade with XP/daily-mission integration; the landing's Mind-Hack Autopsy card now opens the lab in place instead of bouncing to /app ("Open full analysis" remains the escape hatch). - New src/lib/headlineScores.js maps getBusinessMetrics to the four headline scores; score card, share text and share preview now show the same four rows so the playground and shared cards never disagree (this also changes /app's share card rows from Hook/Viral Pull to the headline set). Truth layer: - New src/features/gaugegap/evidence.js: "Export evidence" button on the Butterfly Effect (Lorenz) lab downloads a content-hashed JSON proofpack (schema gaugegap.browser_attractor_proofpack.v1) with live parameters, solver settings, scores, share URL and an explicit claim boundary. - Deep Foundry link from the landing's research section to the published gaugegap-foundry Foundry Experience playground. Tests: new tinyVitest suites for headlineScores and evidence; urgency-goal coverage in rewrite.test.js; scoreCard test updated to the new rows. Stale e2e landing specs (pre-arcade selectors) rewritten against the arcade: content lab local-scan flow asserts no /api/analyze request; 3D-mount and Reconstruct-nav specs target the current DOM. Full suite: 79 unit tests, 13 e2e passed locally. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01725FqR5KopKJJXN3RTfLpD --- brainsnn-r3f-app/src/app/GaugeGapLanding.jsx | 6 +- .../src/features/export/ShareDialog.jsx | 7 +- .../src/features/export/SharePreview.jsx | 11 +- .../src/features/export/scoreCard.js | 15 +- .../src/features/export/scoreCard.test.js | 6 +- .../features/gaugegap/AttractorPlayground.jsx | 10 +- .../features/gaugegap/ContentReactionLab.jsx | 200 +++++++++++++ .../features/gaugegap/ExperimentArcade.jsx | 12 +- .../src/features/gaugegap/evidence.js | 67 +++++ .../src/features/gaugegap/evidence.test.js | 41 +++ .../src/features/gaugegap/useContentScan.js | 57 ++++ .../src/features/improve/rewrite.js | 3 + .../src/features/improve/rewrite.test.js | 7 + brainsnn-r3f-app/src/lib/headlineScores.js | 21 ++ .../src/lib/headlineScores.test.js | 46 +++ brainsnn-r3f-app/src/styles/arcade.css | 283 ++++++++++++++++++ brainsnn-r3f-app/tests/e2e/brainsnn.spec.ts | 52 ++-- 17 files changed, 800 insertions(+), 44 deletions(-) create mode 100644 brainsnn-r3f-app/src/features/gaugegap/ContentReactionLab.jsx create mode 100644 brainsnn-r3f-app/src/features/gaugegap/evidence.js create mode 100644 brainsnn-r3f-app/src/features/gaugegap/evidence.test.js create mode 100644 brainsnn-r3f-app/src/features/gaugegap/useContentScan.js create mode 100644 brainsnn-r3f-app/src/lib/headlineScores.js create mode 100644 brainsnn-r3f-app/src/lib/headlineScores.test.js diff --git a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx index f283f25..0adb3f6 100644 --- a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx +++ b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx @@ -147,7 +147,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
-
13 live12 arcade + flagship lab
+
16 live15 arcade + flagship lab
4 pathsClear first steps
ClientCustom pilot pathway
OpenModels and limits
@@ -176,7 +176,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) { - +

The engagement engine

A useful interaction should continue after the first click.

The visual earns attention. The model earns understanding. The shared state brings the next person into the same system.

@@ -195,7 +195,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
diff --git a/brainsnn-r3f-app/src/features/export/ShareDialog.jsx b/brainsnn-r3f-app/src/features/export/ShareDialog.jsx index 2478398..e3dce64 100644 --- a/brainsnn-r3f-app/src/features/export/ShareDialog.jsx +++ b/brainsnn-r3f-app/src/features/export/ShareDialog.jsx @@ -2,7 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Copy, Download, FileJson, FileText, Link2, Share2 } from 'lucide-react'; import { Modal } from '../../components/ui/Modal.jsx'; import { track } from '../../lib/analytics.js'; -import { deriveExecutiveVerdict, getBusinessMetrics } from '../../lib/scoreMapping.js'; +import { deriveExecutiveVerdict } from '../../lib/scoreMapping.js'; +import { getHeadlineScores } from '../../lib/headlineScores.js'; import { ExportCard } from './ExportCard.jsx'; import { SharePreview } from './SharePreview.jsx'; import { downloadBlob, renderScoreCardBlob, shareScoreCard } from './scoreCard.js'; @@ -17,8 +18,8 @@ export function ShareDialog({ open, onClose, result }) { const verdict = result ? deriveExecutiveVerdict(result) : null; const shareText = useMemo(() => { if (!result || !verdict) return ''; - const metricMap = Object.fromEntries(getBusinessMetrics(result).map((metric) => [metric.id, metric.value])); - return `BRAIN SCAN\n"${verdict.headline}"\n\nHook Strength: ${metricMap.hookStrength}\nTrust: ${metricMap.trust}\nViral Pull: ${verdict.viralScore}\nManipulation Risk: ${metricMap.manipulationRisk}\n\nAI-estimated content response from BrainSNN — scan yours free at brainsnn.com`; + const lines = getHeadlineScores(result).map((metric) => `${metric.label}: ${metric.value}`).join('\n'); + return `BRAIN SCAN\n"${verdict.headline}"\n\n${lines}\n\nAI-estimated content response from BrainSNN — scan yours free at brainsnn.com`; }, [result, verdict]); useEffect(() => { diff --git a/brainsnn-r3f-app/src/features/export/SharePreview.jsx b/brainsnn-r3f-app/src/features/export/SharePreview.jsx index 83808b4..2146abd 100644 --- a/brainsnn-r3f-app/src/features/export/SharePreview.jsx +++ b/brainsnn-r3f-app/src/features/export/SharePreview.jsx @@ -1,9 +1,9 @@ import React from 'react'; -import { deriveExecutiveVerdict, getBusinessMetrics } from '../../lib/scoreMapping.js'; +import { deriveExecutiveVerdict } from '../../lib/scoreMapping.js'; +import { getHeadlineScores } from '../../lib/headlineScores.js'; export function SharePreview({ result }) { const verdict = deriveExecutiveVerdict(result); - const metricMap = Object.fromEntries(getBusinessMetrics(result).map((metric) => [metric.id, metric.value])); return (
BRAIN SCAN @@ -12,10 +12,9 @@ export function SharePreview({ result }) {

AI-estimated content response

-
Hook Strength
{metricMap.hookStrength}
-
Trust
{metricMap.trust}
-
Viral Pull
{verdict.viralScore}
-
Manipulation Risk
{metricMap.manipulationRisk}
+ {getHeadlineScores(result).map((metric) => ( +
{metric.label}
{metric.value}
+ ))}
brainsnn.com
diff --git a/brainsnn-r3f-app/src/features/export/scoreCard.js b/brainsnn-r3f-app/src/features/export/scoreCard.js index 14db653..b72fa86 100644 --- a/brainsnn-r3f-app/src/features/export/scoreCard.js +++ b/brainsnn-r3f-app/src/features/export/scoreCard.js @@ -1,6 +1,7 @@ // Shareable score card: composes the text payload (pure, unit-testable) and // renders a 1200x630 branded PNG. Extends the previous ShareDialog downloadPng. -import { deriveExecutiveVerdict, getBusinessMetrics } from '../../lib/scoreMapping.js'; +import { deriveExecutiveVerdict } from '../../lib/scoreMapping.js'; +import { getHeadlineScores } from '../../lib/headlineScores.js'; import { synchronyPalette } from '../brain3d/mapResultToBrain.js'; import { BRAIN_REGIONS } from '../brain3d/brainRegions.js'; import { mapResultToActivities } from '../brain3d/mapResultToBrain.js'; @@ -9,7 +10,7 @@ export function composeScoreCardText(result = {}) { // Explicit null bypasses the default parameter. const safeResult = result || {}; const verdict = deriveExecutiveVerdict(safeResult); - const metricMap = Object.fromEntries(getBusinessMetrics(safeResult).map((metric) => [metric.id, metric.value])); + const headlineMap = Object.fromEntries(getHeadlineScores(safeResult).map((metric) => [metric.label, metric.value])); const raw = String(safeResult.rawContent || '').replace(/\s+/g, ' ').trim(); const excerpt = raw.length > 90 ? `${raw.slice(0, 87).trimEnd()}…` : raw; return { @@ -18,11 +19,13 @@ export function composeScoreCardText(result = {}) { viralScore: verdict.viralScore, viralLabel: verdict.viralLabel, excerpt, + // The same four headline scores the playground shows, so shared cards and + // the live simulator never disagree. metricRows: [ - ['Hook Strength', metricMap.hookStrength, 'good'], - ['Trust', metricMap.trust, 'good'], - ['Viral Pull', verdict.viralScore, 'good'], - ['Manipulation Risk', metricMap.manipulationRisk, 'risk'], + ['Attention', headlineMap.Attention, 'good'], + ['Trust', headlineMap.Trust, 'good'], + ['Emotional Charge', headlineMap['Emotional Charge'], 'good'], + ['Manipulation Risk', headlineMap['Manipulation Risk'], 'risk'], ], footer: 'brainsnn.com', disclaimer: 'AI-estimated content response', diff --git a/brainsnn-r3f-app/src/features/export/scoreCard.test.js b/brainsnn-r3f-app/src/features/export/scoreCard.test.js index b067986..3284d7b 100644 --- a/brainsnn-r3f-app/src/features/export/scoreCard.test.js +++ b/brainsnn-r3f-app/src/features/export/scoreCard.test.js @@ -16,11 +16,11 @@ describe('composeScoreCardText', () => { expect(card.excerpt.endsWith('…')).toBe(true); }); - it('includes viral pull and manipulation risk rows', () => { + it('includes the four playground headline rows', () => { const card = composeScoreCardText(mockResult); const labels = card.metricRows.map(([label]) => label); - expect(labels).toContain('Viral Pull'); - expect(labels).toContain('Manipulation Risk'); + expect(labels).toEqual(['Attention', 'Trust', 'Emotional Charge', 'Manipulation Risk']); + for (const [, value] of card.metricRows) expect(Number.isFinite(value)).toBe(true); expect(card.footer).toBe('brainsnn.com'); }); diff --git a/brainsnn-r3f-app/src/features/gaugegap/AttractorPlayground.jsx b/brainsnn-r3f-app/src/features/gaugegap/AttractorPlayground.jsx index 8a16a0f..27ffe14 100644 --- a/brainsnn-r3f-app/src/features/gaugegap/AttractorPlayground.jsx +++ b/brainsnn-r3f-app/src/features/gaugegap/AttractorPlayground.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { Copy, Download, Pause, Play, RotateCcw, Share2, Sparkles, Video } from 'lucide-react'; +import { Copy, Download, FileJson, Pause, Play, RotateCcw, Share2, Sparkles, Video } from 'lucide-react'; +import { buildAttractorEvidence, downloadJson } from './evidence.js'; const PRESETS = [ { id: 'classic', label: 'Classic butterfly', sigma: 10, rho: 28, beta: 2.667, speed: 1 }, @@ -288,6 +289,12 @@ export function AttractorPlayground() { { key: 'speed', label: 'Time', min: 0.35, max: 2, step: 0.05 }, ]; + async function exportEvidence() { + const pack = await buildAttractorEvidence({ params, scores, shareUrl: getShareUrl(), controls }); + downloadJson(`gaugegap-evidence-attractor-${String(pack.content_hash).slice(0, 8)}.json`, pack); + setNotice('Evidence pack saved: exact parameters, solver settings and a content hash travel with your run.'); + } + return (
@@ -366,6 +373,7 @@ export function AttractorPlayground() { +

{notice}

diff --git a/brainsnn-r3f-app/src/features/gaugegap/ContentReactionLab.jsx b/brainsnn-r3f-app/src/features/gaugegap/ContentReactionLab.jsx new file mode 100644 index 0000000..06495af --- /dev/null +++ b/brainsnn-r3f-app/src/features/gaugegap/ContentReactionLab.jsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { ArrowRight, ClipboardPaste, Eraser, Play, Share2, Sparkles, WandSparkles } from 'lucide-react'; +import { Brain3D } from '../brain3d/Brain3D.jsx'; +import { BrainVisualizer } from '../results/BrainVisualizer.jsx'; +import { ShareDialog } from '../export/ShareDialog.jsx'; +import { createRewrite } from '../improve/rewrite.js'; +import { isKeyboardScanShortcut } from '../scan/keyboard.js'; +import { getHeadlineScores } from '../../lib/headlineScores.js'; +import { getClassicPreset } from '../../lib/classicPresets.js'; +import { compareResults, deriveExecutiveVerdict } from '../../lib/scoreMapping.js'; +import { MAX_SCAN_CHARS } from '../../lib/validation.js'; +import { track } from '../../lib/analytics.js'; +import { DeepLabHeading } from './DeepLabChrome.jsx'; +import { useContentScan, scanContentLocally } from './useContentScan.js'; + +const GENRES = [ + { id: 'ad', label: 'Ad', presetId: 'luxury-scarcity-ad', placeholder: 'Paste ad copy — the promise, the pressure, the ask…' }, + { id: 'tweet', label: 'Tweet', presetId: 'outrage-bait-post', placeholder: 'Paste a post or hook — the first line does the damage…' }, + { id: 'landing', label: 'Landing page', presetId: 'guru-urgency-pitch', placeholder: 'Paste a hero section or pitch — headline to call-to-action…' }, + { id: 'email', label: 'Email', presetId: 'account-phishing-email', placeholder: 'Paste an email — subject line and body both count…' }, +]; + +const REWRITE_ACTIONS = [ + { goal: 'trust', label: 'Build trust' }, + { goal: 'urgency', label: 'Add real urgency' }, + { goal: 'reduce-risk', label: 'Reduce manipulation' }, +]; + +const PLACEHOLDER_TILES = ['Attention', 'Trust', 'Emotional Charge', 'Manipulation Risk'].map((label) => ({ id: `pending-${label}`, label })); + +function ScoreTiles({ scores, previousScores }) { + const previousById = previousScores ? Object.fromEntries(previousScores.map((score) => [score.id, score.value])) : {}; + return ( +
+ {(scores || PLACEHOLDER_TILES).map((score) => { + const pending = score.value == null; + const before = previousById[score.id]; + const delta = !pending && before != null ? Math.round(score.value - before) : 0; + const improved = score.direction === 'lower-good' ? delta < 0 : delta > 0; + return ( +
+ {score.label} + {pending ? '—' : score.value} + {!pending && delta !== 0 ? ( + 0 ? 'up' : 'down'} ${Math.abs(delta)} since the previous run`}> + {delta > 0 ? '▲' : '▼'} {Math.abs(delta)} + + ) : null} + {pending ? 'Run a simulation' : score.detail || score.explanation} +
+ ); + })} +
+ ); +} + +export function ContentReactionLab({ onOpenScanner }) { + const { input, setInput, status, result, previousResult, error, setError, runSimulation, applyContent } = useContentScan(); + const [genreId, setGenreId] = useState('ad'); + const [shareOpen, setShareOpen] = useState(false); + const [rewrite, setRewrite] = useState(null); + + const genre = GENRES.find((entry) => entry.id === genreId) || GENRES[0]; + const scores = result ? getHeadlineScores(result) : null; + const previousScores = previousResult ? getHeadlineScores(previousResult) : null; + const verdict = result ? deriveExecutiveVerdict(result) : null; + const running = status === 'running'; + + function handleRun(content) { + setRewrite(null); + runSimulation(content); + } + + function loadSample() { + const preset = getClassicPreset(genre.presetId); + if (!preset) return; + setRewrite(null); + applyContent(preset.content); + track('gaugegap_content_sample_loaded', { genre: genre.id }); + } + + async function pasteFromClipboard() { + try { + const text = await navigator.clipboard.readText(); + if (text) { + setInput(text.slice(0, MAX_SCAN_CHARS)); + setError(''); + } + } catch { + setError('Clipboard access was blocked — paste into the box instead.'); + } + } + + function makeRewrite({ goal, label }) { + if (!result) return; + const text = createRewrite(input, goal); + const rewrittenResult = scanContentLocally(text); + setRewrite({ goal, label, text, deltas: compareResults(result, rewrittenResult) }); + track('gaugegap_content_rewrite_created', { goal }); + } + + return ( +
+ + +
+
+
+ {GENRES.map((entry) => ( + + ))} +
+