From 4aa48538f64ac342e152fa885677d0bec8b636a3 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Fri, 1 May 2026 23:40:49 -0500 Subject: [PATCH] ux: chat header + greeting + brain avatar + share button + terms + badge Restores inline-JS styles.el (the obfuscated /assets/js/*.js path broke the chat widget) and threads four UX fixes through it: - Chat header: drop the all-caps "NEURON / Live Demo" subtitle, keep just "Neuron" in bold white at 1rem. - Greeting: replace the canned "Hi. I am Neuron. You get 5 questions" with "Hey. What is on your mind?" in all three callsites (open, reset, turnstile-verified). The questions counter is already in the header, so we don't need to repeat it. - Avatar: switch chat avatar from neuron-icon.png to the brain mark (/assets/brand/neuron-brain.png). Thinking state now shows the pulsing brain plus three-dot animation instead of "thinking..." text. - Share button: from a muted t3 link to a navy pill with subtle background, uppercase letter-spacing, and active-press feedback. main.el: drop the FOUNDING_SOLD = 47 floor and rewrite fetch_founding_count_stripe to count succeeded PaymentIntents (the live Stripe Elements path) AND legacy Checkout Sessions, excluding refunded charges. founding_badge.el: rewrite to the Option C "Tag" design picked from /tmp/founding-badge-preview.html - tall card, navy top stroke, brain mark prominent, "Neuron, LLC" footer. terms.el: kill the "Neuron Inference - our own inference layer, live now" claim. Inference launches Q3 2026 and the wording now says so. --- src/founding_badge.el | 166 ++++++++++++++++-------------------------- src/main.el | 65 ++++++++++------- src/styles.el | 114 ++++++++++++++++++++--------- src/terms.el | 2 +- 4 files changed, 182 insertions(+), 165 deletions(-) diff --git a/src/founding_badge.el b/src/founding_badge.el index afef33e..66097c5 100644 --- a/src/founding_badge.el +++ b/src/founding_badge.el @@ -1,63 +1,25 @@ -// founding_badge.el - Founding Member badge component. +// founding_badge.el - Founding Member badge (Option C: Tag). // -// Renders the certificate-style badge with guilloché background, -// navy colour scheme, and member number. Matches the React archive design. -// Used on the post-purchase success page and as a shareable /founding-badge route. +// Tall card, navy top stroke, brain logo prominent, member number, +// "Neuron, LLC" footer. Matches the design picked from +// /tmp/founding-badge-preview.html. fn founding_badge(member_number: Int) -> String { let num_str: String = int_to_str(member_number) let show_number: Bool = member_number > 0 let number_html: String = if show_number { - "
- No. -

" + num_str + "

-

of 1,000

-
" + "
#" + num_str + "
+
of 1,000
" } else { - "

Your number awaits

" + "
Your number awaits
" } return "
- - - - - - - - - - - - - - - - - - - - - - - - - - - - NEURON TECHNOLOGIES · FOUNDING MEMBER · NEURON TECHNOLOGIES · FOUNDING MEMBER - NEURON TECHNOLOGIES · FOUNDING MEMBER · NEURON TECHNOLOGIES · FOUNDING MEMBER - - - -
-

Founding Member

-
-
- \"Neuron\" -
- " + number_html + " -
+ \"Neuron\" +
Founding Member
+ " + number_html + " +
+
Neuron, LLC
" } @@ -65,72 +27,66 @@ fn founding_badge(member_number: Int) -> String { fn founding_badge_css() -> String { return " " diff --git a/src/main.el b/src/main.el index a89af61..442ffbb 100644 --- a/src/main.el +++ b/src/main.el @@ -49,7 +49,7 @@ from account import { account_page } // ── Founding counter ────────────────────────────────────────────────────────── let FOUNDING_TOTAL: Int = 1000 -let FOUNDING_SOLD: Int = 47 // floor — real count comes from Stripe at startup +let FOUNDING_SOLD: Int = 0 // no artificial floor - real count comes from Stripe // ── Founding count helpers ───────────────────────────────────────────────────── @@ -76,35 +76,48 @@ fn fetch_founding_count_stripe(stripe_key: String) -> Int { if str_eq(stripe_key, "") { return FOUNDING_SOLD } - // Fetch all complete checkout sessions and count founding ones. - // Note: metadata[] filter in URL causes 400 — filter client-side instead. - let url: String = "https://api.stripe.com/v1/checkout/sessions?status=complete&limit=100" - let resp: String = http_get_auth(url, stripe_key) - if str_eq(resp, "") { - return FOUNDING_SOLD - } - let data_raw: String = json_get_raw(resp, "data") - if str_eq(data_raw, "") { - return FOUNDING_SOLD - } - // Count only sessions where metadata.plan = "founding" - // json_get uses literal key search, so extract metadata object first - let total: Int = json_array_len(data_raw) let count: Int = 0 - let idx: Int = 0 - while idx < total { - let session: String = json_array_get(data_raw, idx) - // json_get_raw handles multi-line JSON; json_get stops at newlines - let metadata: String = json_get_raw(session, "metadata") - let plan: String = if str_eq(metadata, "") { "" } else { json_get(metadata, "plan") } - if str_eq(plan, "founding") { - let count = count + 1 + + // 1) PaymentIntents (Stripe Elements path - this is what the live site uses) + let pi_url: String = "https://api.stripe.com/v1/payment_intents?limit=100" + let pi_resp: String = http_get_auth(pi_url, stripe_key) + let pi_data: String = if str_eq(pi_resp, "") { "" } else { json_get_raw(pi_resp, "data") } + if !str_eq(pi_data, "") { + let pi_total: Int = json_array_len(pi_data) + let pi_idx: Int = 0 + while pi_idx < pi_total { + let pi: String = json_array_get(pi_data, pi_idx) + let status: String = json_get(pi, "status") + let metadata: String = json_get_raw(pi, "metadata") + let plan: String = if str_eq(metadata, "") { "" } else { json_get(metadata, "plan") } + // Count only fully-charged founding PIs that are not refunded + let amt_refunded: String = json_get(pi, "amount_refunded") + let refunded_n: Int = str_to_int(amt_refunded) + if str_eq(status, "succeeded") && str_eq(plan, "founding") && refunded_n <= 0 { + let count = count + 1 + } + let pi_idx = pi_idx + 1 } - let idx = idx + 1 } - if count <= 0 { - return FOUNDING_SOLD + + // 2) Hosted Checkout Sessions (legacy /api/checkout path - kept for completeness) + let cs_url: String = "https://api.stripe.com/v1/checkout/sessions?status=complete&limit=100" + let cs_resp: String = http_get_auth(cs_url, stripe_key) + let cs_data: String = if str_eq(cs_resp, "") { "" } else { json_get_raw(cs_resp, "data") } + if !str_eq(cs_data, "") { + let cs_total: Int = json_array_len(cs_data) + let cs_idx: Int = 0 + while cs_idx < cs_total { + let session: String = json_array_get(cs_data, cs_idx) + let metadata: String = json_get_raw(session, "metadata") + let plan: String = if str_eq(metadata, "") { "" } else { json_get(metadata, "plan") } + if str_eq(plan, "founding") { + let count = count + 1 + } + let cs_idx = cs_idx + 1 + } } + return count } diff --git a/src/styles.el b/src/styles.el index c49567e..a150af6 100644 --- a/src/styles.el +++ b/src/styles.el @@ -1519,19 +1519,13 @@ fn page_open() -> String { } #neuron-demo-header span { font-family: var(--body); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.12em; - text-transform: uppercase; - } - #neuron-demo-header-sub { - font-family: var(--body); - font-size: 0.625rem; - letter-spacing: 0.08em; - color: rgba(255,255,255,.85); - text-transform: uppercase; - margin-top: 0.1rem; + font-size: 1rem; + font-weight: 700; + color: #fff; + letter-spacing: 0; + text-transform: none; } + #neuron-demo-header-sub { display: none; } #neuron-demo-close { background: none; border: none; @@ -1666,29 +1660,66 @@ fn page_open() -> String { .demo-msg-thinking { align-self: flex-start; color: var(--t3); - font-size: 0.75rem; - letter-spacing: 0.08em; - font-style: italic; + font-size: 0.8rem; + letter-spacing: 0.04em; + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.25rem 0; + } + .demo-msg-thinking .demo-msg-avatar { + width: 1.75rem; + height: 1.75rem; + flex-shrink: 0; + } + .demo-msg-thinking .demo-msg-avatar img { + width: 1.125rem; + height: 1.125rem; + animation: brainPulse 1.4s ease-in-out infinite; + } + .demo-msg-thinking-dots { display: inline-flex; gap: 3px; } + .demo-msg-thinking-dots span { + width: 5px; height: 5px; border-radius: 50%; + background: rgba(0,82,160,0.55); + animation: thinkDot 1.2s ease-in-out infinite; + } + .demo-msg-thinking-dots span:nth-child(2) { animation-delay: 0.15s; } + .demo-msg-thinking-dots span:nth-child(3) { animation-delay: 0.3s; } + @keyframes thinkDot { + 0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); } + 40% { opacity: 1; transform: scale(1); } + } + @keyframes brainPulse { + 0%, 100% { opacity: 0.55; transform: scale(0.92); } + 50% { opacity: 1; transform: scale(1.05); } } .demo-share-pill { - background: none; - border: none; - color: var(--t3); + background: rgba(0,82,160,0.07); + border: 1px solid rgba(0,82,160,0.22); + color: var(--navy); cursor: pointer; - padding: 0.2rem 0; + padding: 0.4rem 0.75rem; font-family: var(--body); font-size: 0.7rem; - font-weight: 400; - letter-spacing: 0.04em; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; display: inline-flex; align-items: center; - gap: 0.25rem; - margin-top: 0.3rem; + gap: 0.4rem; + margin-top: 0.5rem; + margin-left: 2.5rem; align-self: flex-start; - transition: color 0.15s; + transition: background 0.15s, border-color 0.15s, transform 0.1s; line-height: 1; + border-radius: 6px; } - .demo-share-pill:hover { color: var(--navy); } + .demo-share-pill:hover { + background: rgba(0,82,160,0.14); + border-color: rgba(0,82,160,0.45); + } + .demo-share-pill:active { transform: scale(0.97); } + .demo-share-pill svg { width: 11px; height: 11px; } #neuron-demo-input-row { display: flex; @@ -1993,7 +2024,7 @@ fn page_close() -> String {
@@ -2001,7 +2032,7 @@ fn page_close() -> String {
- \"Neuron\" + \"Neuron\"
Neuron
Live Demo
@@ -2118,7 +2149,7 @@ fn page_close() -> String { if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } var btn = document.getElementById('neuron-demo-send'); if (btn) btn.disabled = false; - addMsg('ai', 'Hi. I am Neuron. You get 5 questions.', true); + addMsg('ai', 'Hey. What is on your mind?', true); }; window.neuronDemoToggle = function() { @@ -2139,7 +2170,7 @@ fn page_close() -> String { if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; } } } else { - addMsg('ai', 'Hi. I am Neuron. You get 5 questions.', true); + addMsg('ai', 'Hey. What is on your mind?', true); } } var input = document.getElementById('neuron-demo-text'); @@ -2168,7 +2199,7 @@ fn page_close() -> String { if (msgs) msgs.style.display = 'flex'; if (inputRow) inputRow.style.display = 'flex'; // Show opening message - addMsg('ai', 'Hi. I am Neuron. You get 5 questions.', true); + addMsg('ai', 'Hey. What is on your mind?', true); updateCountdown(); var inp = document.getElementById('neuron-demo-text'); if (inp) inp.focus(); @@ -2192,7 +2223,7 @@ fn page_close() -> String { avatar.className = 'demo-msg-avatar'; if (role === 'ai') { var img = document.createElement('img'); - img.src = '/assets/neuron-icon.png'; + img.src = '/assets/brand/neuron-brain.png'; img.alt = 'Neuron'; avatar.appendChild(img); } else { @@ -2275,8 +2306,25 @@ fn page_close() -> String { input.value = ''; btn.disabled = true; addMsg('user', msg); - var thinking = addMsg('thinking', 'thinking...'); - if (thinking) thinking.className = 'demo-msg demo-msg-thinking'; + // Thinking indicator with brain avatar + animated dots + var thinking = document.createElement('div'); + thinking.className = 'demo-msg demo-msg-thinking'; + var thAvatar = document.createElement('div'); + thAvatar.className = 'demo-msg-avatar'; + var thImg = document.createElement('img'); + thImg.src = '/assets/brand/neuron-brain.png'; + thImg.alt = 'Neuron'; + thAvatar.appendChild(thImg); + thinking.appendChild(thAvatar); + var thDots = document.createElement('span'); + thDots.className = 'demo-msg-thinking-dots'; + thDots.innerHTML = ''; + thinking.appendChild(thDots); + var thMsgsEl = document.getElementById('neuron-demo-messages'); + if (thMsgsEl) { + thMsgsEl.appendChild(thinking); + thMsgsEl.scrollTop = thMsgsEl.scrollHeight; + } if (turnstileVerified && !session._cfSent) { session._cfSent = true; } try { // Build history from session for soul context diff --git a/src/terms.el b/src/terms.el index 0ae075c..668ca87 100644 --- a/src/terms.el +++ b/src/terms.el @@ -59,7 +59,7 @@ fn terms_body() -> String {

Inference

Neuron routes inference to whatever provider you configure - your own keys for OpenAI, Anthropic, Grok, any compatible endpoint. When you use a third-party provider, your prompts go through their infrastructure. Their terms apply. I don't control what they do with it.

-

Neuron Inference - our own inference layer, live now - doesn't store your requests, doesn't train on them, doesn't share them. A separate data addendum covers the details.

+

Neuron Inference - our own inference layer, launching Q3 2026 - will not store your requests, will not train on them, and will not share them. A separate data addendum will cover the details when it launches.