migrate stage build to native elc; chat restores from localStorage on return

Build pipeline
- build-stage.sh replaces the old in-Dockerfile bootstrap.py path. Host
  pre-compiles src/*.el into dist/main.c via the canonical native elc at
  foundation/el/dist/platform/elc and applies the stub-decl sed before
  docker buildx runs.
- Dockerfile.stage drops bootstrap.py + python3 from the builder stage
  and just runs cc on the host-supplied dist/main.c.
- Pre-rendered HTML shells under /srv/landing/ are now chowned to the
  landing user so the El page-builder's fs_write at startup can rewrite
  them — without that, post-COPY edits never reach the served HTML and
  the served page stays as the stale build-time fallback.

Chat restore
- session.verified + session.verifiedAt persist through localStorage so
  a return visit within 24h skips the Turnstile gate and lands directly
  in the restored conversation.
- restoreOrGreet() is the single source of truth for what shows up in
  the message pane after the gate clears: replays prior messages with
  skipSave, else drops the canned hello once and remembers it.
- applyVerifiedDom() hides the gate / reveals the chat row, called both
  from the verified-on-load path (DOMContentLoaded if loading, else
  immediate) and from the Turnstile callback.
- neuronDemoReset clears verified + verifiedAt so the gate returns next
  open.

Extracted JS assets (src/assets/js/*.js + manifest.json) and the
extract-js.py helper land here too — they match what the new build-stage
flow produces and removes the inline <script> blobs from the served HTML.
This commit is contained in:
Will Anderson
2026-05-02 11:15:09 -05:00
parent cae5028130
commit 640813e42e
27 changed files with 906 additions and 1461 deletions
+14 -17
View File
@@ -4,8 +4,10 @@
# - neuron-web on port 8080 (landing page server)
# - soul-demo on port 7772 (demo chat, localhost only)
#
# Both binaries are compiled from C inside Docker for linux/amd64.
# The engram snapshot is baked in so the soul has memory from first boot.
# bootstrap.py is no longer in the build path. The host-side build-stage.sh
# pre-compiles src/*.el → dist/main.c using the canonical native elc and
# applies the stub forward-declaration sed before this Dockerfile runs.
# The image just compiles the finished C source.
# ── Stage 1: compile both binaries ────────────────────────────────────────────
FROM debian:bookworm-slim AS builder
@@ -15,7 +17,6 @@ RUN apt-get update \
build-essential \
libcurl4-openssl-dev \
libssl-dev \
python3 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
@@ -26,22 +27,13 @@ COPY runtime/el_runtime.c runtime/el_runtime.h ./
# ── Build neuron-web ──────────────────────────────────────────────────────────
#
# Inline-JS extraction (scripts/extract-js.py) is expected to run BEFORE the
# wrapper concatenates src/*.el into dist/main-combined.el. That side of the
# pipeline lives in build-local.sh (gated by EXTRACT_JS=1) and the outer
# orchestrator. By the time we reach this Dockerfile, main-combined.el
# already references /assets/js/<hash>.js and the corresponding asset files
# have been emitted under src/assets/js/. The COPY of src/assets at the
# runtime stage below is what ships those files into the container.
# main.c was generated on the host by build-stage.sh from src/*.el via the
# native elc compiler. Stub forward-declarations were already injected on
# the host side, so this stage is a straight cc invocation.
COPY dist/web_stubs.c ./
COPY dist/bootstrap.py ./
COPY dist/main-combined.el ./
COPY dist/main.c ./
RUN python3 bootstrap.py main-combined.el > main.c && \
sed -i \
's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);|' \
main.c && \
cc -O2 -rdynamic \
RUN cc -O2 -rdynamic \
-o neuron-web \
main.c web_stubs.c el_runtime.c \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
@@ -79,7 +71,12 @@ COPY src/assets /srv/landing/assets
COPY src/llms.txt /srv/landing/llms.txt
# Pre-rendered HTML shells (about, terms, enterprise-terms, index) used as
# fallback when the El page-builder hasn't been seeded yet at startup.
# chown to the landing user so the El runtime's fs_write at startup can
# rewrite them with the freshly-rendered page (extracted JS asset paths,
# updated chat widget, etc.). Without this they stay as their COPY'd root-
# owned shells and the served HTML never reflects post-COPY source edits.
COPY src/about.html src/terms.html src/enterprise-terms.html src/index.html /srv/landing/
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
COPY dist/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
Executable
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
#
# build-stage.sh — Build the Stage marketing image (neuron-web + soul-demo).
#
# Pipeline:
# 1. Stage the foundation El runtime into ./runtime/.
# 2. Concatenate src/*.el into dist/main-combined.el (component-first,
# main.el last; matches the historical order from build-local.sh).
# 3. Compile dist/main-combined.el → dist/main.c using the canonical
# native elc at foundation/el/dist/platform/elc.
# 4. Inject the host-side stub forward declarations into dist/main.c
# (sed header rewrite, same set as the prior in-Dockerfile sed).
# 5. docker buildx build --platform linux/amd64 -f Dockerfile.stage.
#
# bootstrap.py is no longer in the build path. The container image now
# expects dist/main.c to be a finished C source — it just runs cc on it.
#
# Inline-JS extraction is gated by EXTRACT_JS=1 just like build-local.sh
# was. Production deploys should always extract.
#
# Usage:
# ./build-stage.sh <tag> — build marketing:<tag>
# EXTRACT_JS=1 ./build-stage.sh X — also extract inline JS to assets
set -euo pipefail
cd "$(dirname "$0")"
TAG="${1:-dev}"
LANDING_DIR=$(pwd)
EL_HOME="${EL_HOME:-${LANDING_DIR}/../../foundation/el}"
ELC="${EL_HOME}/dist/platform/elc"
RUNTIME_SRC="${EL_HOME}/el-compiler/runtime"
if [ ! -x "${ELC}" ]; then
echo "elc not found at ${ELC}" >&2
exit 1
fi
echo "==> Staging El runtime from ${RUNTIME_SRC}"
mkdir -p runtime dist
cp "${RUNTIME_SRC}/el_runtime.c" runtime/
cp "${RUNTIME_SRC}/el_runtime.h" runtime/
# Optional inline-JS extraction. Off by default for fast dev iteration; the
# script is idempotent so flipping the flag on a prior tree just reuses
# previously-extracted assets.
if [[ "${EXTRACT_JS:-0}" == "1" ]]; then
echo "==> Extracting inline JS → src/assets/js/"
if [ ! -x "node_modules/.bin/terser" ] || [ ! -x "node_modules/.bin/javascript-obfuscator" ]; then
echo " installing terser + javascript-obfuscator (no-save)..."
npm install --no-save --silent terser javascript-obfuscator
fi
python3 scripts/extract-js.py
fi
echo "==> Combining El sources → dist/main-combined.el"
COMPONENTS=(nav hero pillars how_it_works inference efficiency comparison
environmental enterprise mission local_first pricing marketplace viral
footer styles about founding_badge terms enterprise_terms checkout safety
gallery account)
{
for f in "${COMPONENTS[@]}"; do
if [ -f "src/${f}.el" ]; then
grep -hv '^[[:space:]]*from\|^[[:space:]]*import' "src/${f}.el"
echo ""
fi
done
grep -v '^from\|^import' src/main.el
} > dist/main-combined.el
echo " $(wc -l < dist/main-combined.el) lines"
echo "==> Compiling dist/main-combined.el → dist/main.c via ${ELC}"
"${ELC}" dist/main-combined.el > dist/main.c
echo " $(wc -l < dist/main.c) lines of C"
echo "==> Injecting host-side stub forward declarations"
# GNU vs BSD sed: -i with no arg works on GNU, breaks on macOS BSD sed
# (BSD requires -i ''). Detect and branch.
SED_INPLACE=(-i)
if sed --version >/dev/null 2>&1; then
SED_INPLACE=(-i)
else
SED_INPLACE=(-i '')
fi
sed "${SED_INPLACE[@]}" \
's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);|' \
dist/main.c
echo "==> Building Docker image marketing:${TAG} for linux/amd64"
docker buildx build --platform linux/amd64 --load \
-f Dockerfile.stage \
-t "marketing:${TAG}" \
.
echo "==> Done. marketing:${TAG} built."
+37
View File
@@ -4840,6 +4840,43 @@ el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
return el_wrap_str(b.buf);
}
/* engram_scan_nodes_by_type_json — filter by node_type before paginating.
* Empty / NULL type_v falls back to the unfiltered scan (existing behaviour).
* Result is JSON array, salience-sorted, transparent layers skipped. */
el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) {
const char* type_filter = EL_CSTR(type_v);
if (!type_filter || !*type_filter) {
return engram_scan_nodes_json(limit, offset);
}
EngramStore* g = engram_get();
int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100;
int64_t off = (int64_t)offset; if (off < 0) off = 0;
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t));
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
int64_t live = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue;
const char* nt = g->nodes[i].node_type;
if (!nt || strcmp(nt, type_filter) != 0) continue;
idx[live++] = i;
}
engram_sort_indices_by_salience(idx, live, g->nodes);
int64_t end = off + lim;
if (end > live) end = live;
int first = 1;
for (int64_t i = off; i < end; i++) {
if (!first) jb_putc(&b, ',');
engram_emit_node_json(&b, &g->nodes[idx[i]]);
first = 0;
}
free(idx);
jb_putc(&b, ']');
return el_wrap_str(b.buf);
}
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
/* Re-implement here directly so we serialize without going through
* the ElList path. Walks BFS to max_depth, emits {node, edge, hops}
+464
View File
@@ -0,0 +1,464 @@
#!/usr/bin/env python3
"""
extract-js.py — Extract inline <script> blocks from El source files into
external, minified, obfuscated .js files served from /assets/js/.
Why
---
The El landing page embeds JavaScript inline as escaped string literals.
That bloats the HTML payload and exposes implementation. This script
extracts each substantial inline block to a hashed file under
src/assets/js/, replaces the El-side block with
`<script src="/assets/js/<hash>.js" defer></script>`, and writes a manifest
for cache-busting.
Behaviour
---------
- Skips `<script src=...>` external loaders (kept as-is).
- Skips `<script type="application/ld+json">` (data, not code).
- Skips inline blocks shorter than MIN_INLINE_BYTES (defaults to 200).
- Handles El-side runtime interpolation `'" + var + "'`. For each
interpolated identifier the extractor emits a tiny inline shim
`<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.<id>="\"+id+\"";</script>`
immediately before the external script tag, and rewrites the JS body
to read from `window.NEURON_CFG.<id>` so the external file is fully
static and runtime values are still injected at render time.
- Pipeline per file: terser (compress + mangle, reserved globals
preserved) → javascript-obfuscator (string-array, base64, hex names).
Idempotency
-----------
- Running twice is a no-op: blocks already rewritten to
`<script src="/assets/js/...">` are not re-extracted.
- Filenames are content-hashed (sha1[:12]), so unchanged source produces
the same output filename and an unchanged manifest.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Optional
# ── Paths ────────────────────────────────────────────────────────────────────
REPO_ROOT = Path(__file__).resolve().parent.parent
SRC_DIR = REPO_ROOT / "src"
ASSET_DIR = SRC_DIR / "assets" / "js"
MANIFEST = ASSET_DIR / "manifest.json"
# Prefer locally installed binaries; fall back to npx.
NODE_BIN = REPO_ROOT / "node_modules" / ".bin"
TERSER = str(NODE_BIN / "terser") if (NODE_BIN / "terser").exists() else "npx terser"
OBFUSCATOR = (
str(NODE_BIN / "javascript-obfuscator")
if (NODE_BIN / "javascript-obfuscator").exists()
else "npx javascript-obfuscator"
)
# ── Config ───────────────────────────────────────────────────────────────────
MIN_INLINE_BYTES = 200 # below this we keep inline (analytics shims, redirects)
# Globals referenced from outside the script (HTML onclick=, onchange=, etc).
# These names cannot be mangled or obfuscated or buttons stop working.
RESERVED_GLOBALS = [
# Chat widget
"neuronDemoToggle",
"neuronDemoSend",
"neuronDemoReset",
# Auth flows (account + checkout)
"signInWith",
"signInWithEmail",
"signUpWithEmail",
"signOut",
"resetPassword",
"sendResetEmail",
"updatePassword",
"showSignIn",
"showSignUp",
"hideReset",
# Misc handlers
"setSort",
"addFamilyMember",
"removeFamilyMember",
"copyForPlatform",
"entHeadcountChange",
# Runtime config bootstrap (do not let obfuscator mangle this name)
"NEURON_CFG",
]
# Files to scan. The extractor walks every .el file in src/ but we filter
# to skip the leaf component files known to contain no <script> blocks.
EL_FILES = sorted(SRC_DIR.glob("*.el"))
# ── El extraction ────────────────────────────────────────────────────────────
# El uses backslash-escaped quotes inside its string literals. Inside an
# El string the JS body looks like:
# <script>\n var x = '\"hello\"';\n</script>
# i.e. quotes are written as \". We unescape on the way out, re-escape on
# the way in.
# We match a *plain* opening <script> tag followed by JS body and </script>.
# Cases we deliberately don't match:
# - <script src=...>...</script> (external loader)
# - <script async ...>...</script> (external loader, even with body)
# - <script type="application/ld+json">...</script> (structured data)
SCRIPT_BLOCK_RE = re.compile(
r"<script>\s*\n(.*?)\n\s*</script>",
re.DOTALL,
)
# An interpolation point inside a JS body: `'" + ident + "'` (single-quoted
# string in JS containing an El concat). We capture the bare identifier.
INTERP_RE = re.compile(r"""'"\s*\+\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*"'""")
def is_skip_block(body: str) -> bool:
"""True if the block is too small or non-JS to be worth extracting."""
stripped = body.strip()
if len(stripped) < MIN_INLINE_BYTES:
return True
return False
def el_unescape(s: str) -> str:
r"""Mirror the El lexer's string-escape rules (foundation/el/bootstrap.py):
\n -> LF, \t -> TAB, \r -> CR, \" -> ", \\ -> \, \<X> -> X for any X.
The catch-all means \' inside an El string yields a bare apostrophe;
if we don't replicate that here, an extracted block like
`onclick=\"window.location.href=\\\'/contact\\\'\"` parses with stray
backslashes that terser then rejects as bad escape sequences."""
out = []
i = 0
n = len(s)
while i < n:
c = s[i]
if c == "\\" and i + 1 < n:
nxt = s[i + 1]
if nxt == "n":
out.append("\n")
elif nxt == "t":
out.append("\t")
elif nxt == "r":
out.append("\r")
elif nxt == '"':
out.append('"')
elif nxt == "\\":
out.append("\\")
else:
# Catch-all: unrecognised escape collapses to the second char,
# exactly as the El lexer does.
out.append(nxt)
i += 2
continue
out.append(c)
i += 1
return "".join(out)
def el_escape_attr(s: str) -> str:
"""Escape a string for use inside an El "..." literal. We only need to
escape the double quote — backslash is already legal in URLs and we
don't emit any."""
return s.replace("\\", "\\\\").replace('"', '\\"')
def sha12(content: str) -> str:
return hashlib.sha1(content.encode("utf-8")).hexdigest()[:12]
def run(cmd: List[str], **kwargs) -> subprocess.CompletedProcess:
proc = subprocess.run(cmd, check=False, capture_output=True, text=True, **kwargs)
if proc.returncode != 0:
sys.stderr.write(
f"\n[extract-js] command failed: {' '.join(cmd[:2])} ...\n"
f" exit={proc.returncode}\n"
f" stdout: {proc.stdout[:500]}\n"
f" stderr: {proc.stderr[:2000]}\n"
)
raise subprocess.CalledProcessError(
proc.returncode, cmd, proc.stdout, proc.stderr
)
return proc
def minify_and_obfuscate(js: str, hash_id: str) -> str:
"""Run js through terser then javascript-obfuscator. Returns the final
obfuscated source."""
raw_path = ASSET_DIR / f".{hash_id}.raw.js"
min_path = ASSET_DIR / f".{hash_id}.min.js"
out_path = ASSET_DIR / f"{hash_id}.js"
def _cleanup_scratch() -> None:
raw_path.unlink(missing_ok=True)
min_path.unlink(missing_ok=True)
raw_path.write_text(js, encoding="utf-8")
reserved_arg = ",".join(RESERVED_GLOBALS)
# terser
terser_cmd = TERSER.split() + [
str(raw_path),
"--compress",
"passes=2,drop_console=true,drop_debugger=true",
"--mangle",
f"reserved=[{reserved_arg}]",
"--output",
str(min_path),
]
try:
run(terser_cmd)
except Exception:
_cleanup_scratch()
raise
# javascript-obfuscator
obf_cmd = OBFUSCATOR.split() + [
str(min_path),
"--output",
str(out_path),
"--compact",
"true",
"--simplify",
"true",
"--string-array",
"true",
"--string-array-encoding",
"base64",
"--string-array-threshold",
"0.75",
"--identifier-names-generator",
"hexadecimal",
"--rename-globals",
"false",
"--self-defending",
"false",
"--reserved-names",
",".join(RESERVED_GLOBALS),
]
try:
run(obf_cmd)
except Exception:
_cleanup_scratch()
raise
# Tidy up scratch files; keep only the final .js
_cleanup_scratch()
return out_path.read_text(encoding="utf-8")
def find_script_blocks(text: str) -> List[tuple[int, int, str]]:
"""Return (start, end, body) for every plain <script>…</script> block.
`start`/`end` are file offsets covering the entire match (the tags
too)."""
out: List[tuple[int, int, str]] = []
for m in SCRIPT_BLOCK_RE.finditer(text):
out.append((m.start(), m.end(), m.group(1)))
return out
def process_block(raw_body_escaped: str) -> Optional[tuple[str, str, List[str]]]:
"""Process a single <script> body.
Returns (hash_id, replacement_html_el_escaped, interpolated_ids) or
None if the block should remain inline.
The replacement HTML is already El-escaped (so it can be slotted back
into the El source string verbatim).
"""
if is_skip_block(raw_body_escaped):
return None
# Convert El-string-escaped JS into real JS source.
js_with_interp = el_unescape(raw_body_escaped)
# Find interpolation identifiers and rewrite them to read from
# window.NEURON_CFG.<id>. We dedupe in occurrence order.
seen: List[str] = []
def repl(m: re.Match) -> str:
ident = m.group(1)
if ident not in seen:
seen.append(ident)
return f"window.NEURON_CFG.{ident}"
js_static = INTERP_RE.sub(repl, js_with_interp)
# If interpolation existed, we need to wrap the JS body so it reads
# the runtime config. Strings come back as JS strings, so we just
# inject `var X = window.NEURON_CFG.X` shims to keep call sites
# readable. Actually simpler: leave the call sites as
# `window.NEURON_CFG.X` — that's already what `repl` produced, and
# the original had `'" + var + "'` (a string), so the new value is a
# string too.
#
# Hash + minify the static JS.
hash_id = sha12(js_static)
minify_and_obfuscate(js_static, hash_id)
# Build replacement HTML for the El source.
parts: List[str] = []
if seen:
# Inline shim: bootstrap window.NEURON_CFG with runtime values.
# Each line: window.NEURON_CFG.<id> = "<el-interp>";
cfg_assigns = "".join(
f'window.NEURON_CFG.{ident}=\\"" + {ident} + "\\";'
for ident in seen
)
# The shim itself lives inline in the El string. The `\"` are
# already the right escape for the surrounding El "..." literal.
shim = (
"<script>window.NEURON_CFG=window.NEURON_CFG||{};"
+ cfg_assigns
+ "</script>"
)
parts.append(shim)
# External script tag, defer so it runs after parse but before
# DOMContentLoaded — that's compatible with `onclick=` handlers
# because they only fire on user interaction (post-load).
parts.append(
f'<script src=\\"/assets/js/{hash_id}.js\\" defer></script>'
)
return hash_id, "".join(parts), seen
EXISTING_REF_RE = re.compile(
r'<script\s+src=\\"/assets/js/([0-9a-f]{12})\.js\\"\s+defer></script>'
)
def collect_existing_refs(text: str) -> List[str]:
"""Find /assets/js/<hash>.js references already inlined into this El
file from a previous run. Returns hash IDs in document order."""
return [m.group(1) for m in EXISTING_REF_RE.finditer(text)]
def process_file(path: Path) -> tuple[int, int, List[dict]]:
"""Rewrite a single .el file, replacing extractable <script> blocks.
Returns (blocks_extracted, bytes_saved, manifest_entries). Manifest
entries always include both newly-extracted *and* previously-extracted
references so the manifest stays a complete inventory across reruns.
"""
text = path.read_text(encoding="utf-8")
original_len = len(text)
# Pick up existing references first (idempotency).
existing: List[dict] = []
for h in collect_existing_refs(text):
asset_path = ASSET_DIR / f"{h}.js"
if asset_path.exists():
existing.append(
{
"file": path.name,
"hash": h,
"asset": f"/assets/js/{h}.js",
"size": asset_path.stat().st_size,
"interpolated": [],
"note": "carried from prior run",
}
)
blocks = find_script_blocks(text)
if not blocks:
return 0, 0, existing
extracted = 0
new_entries: List[dict] = []
# Walk in reverse so offsets remain valid as we splice.
for start, end, body in reversed(blocks):
result = process_block(body)
if result is None:
continue
hash_id, replacement, interp_ids = result
text = text[:start] + replacement + text[end:]
extracted += 1
new_entries.append(
{
"file": path.name,
"hash": hash_id,
"asset": f"/assets/js/{hash_id}.js",
"size": (ASSET_DIR / f"{hash_id}.js").stat().st_size,
"interpolated": interp_ids,
}
)
if extracted:
path.write_text(text, encoding="utf-8")
bytes_saved = original_len - len(text)
return extracted, bytes_saved, existing + new_entries
def main() -> int:
ASSET_DIR.mkdir(parents=True, exist_ok=True)
total_blocks = 0
total_saved = 0
all_entries: List[dict] = []
for el in EL_FILES:
n, saved, entries = process_file(el)
if n:
print(
f" {el.name:25s} {n} block(s) extracted, "
f"{saved:6d} bytes pulled out"
)
total_blocks += n
total_saved += saved
# Always carry entries forward so the manifest is a complete
# inventory even when this run extracted zero new blocks.
all_entries.extend(entries)
# Sort manifest for stable output regardless of file walk order.
all_entries.sort(key=lambda e: (e["file"], e["hash"]))
# Garbage-collect orphan .js files in the asset dir whose hash is no
# longer referenced by any El source. Without this, edits to the
# original JS leave stale hashed files behind forever.
keep = {f"{e['hash']}.js" for e in all_entries}
keep.add("manifest.json")
removed: List[str] = []
for f in ASSET_DIR.iterdir():
if f.is_file() and f.name not in keep and not f.name.startswith("."):
f.unlink()
removed.append(f.name)
if removed:
print(f" pruned {len(removed)} orphan asset(s): {', '.join(sorted(removed))}")
MANIFEST.write_text(
json.dumps(
{
"generated_by": "scripts/extract-js.py",
"count": len(all_entries),
"entries": all_entries,
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
print(
f"\n total: {total_blocks} block(s), "
f"{total_saved} bytes removed from El sources, "
f"{len(all_entries)} asset(s) → {ASSET_DIR.relative_to(REPO_ROOT)}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -348
View File
@@ -896,354 +896,7 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
</div>
<script src=\"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js\"></script>
<script>
(function() {
'use strict';
var SUPABASE_URL = '" + supabase_url + "';
var SUPABASE_ANON_KEY = '" + supabase_anon_key + "';
var sb = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: { flowType: 'implicit' }
});
// ── Helpers ────────────────────────────────────────────────────────────────
function show(id) {
var el = document.getElementById(id);
if (el) el.style.display = '';
}
function hide(id) {
var el = document.getElementById(id);
if (el) el.style.display = 'none';
}
function setText(id, text) {
var el = document.getElementById(id);
if (el) el.textContent = text;
}
function setHtml(id, html) {
var el = document.getElementById(id);
if (el) el.innerHTML = html;
}
// ── Sign-in ────────────────────────────────────────────────────────────────
window.signInWith = async function(provider) {
var btn = document.getElementById('btn-' + provider);
if (btn) { btn.disabled = true; btn.style.opacity = '0.6'; }
try {
var result = await sb.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo: window.location.origin + '/account'
}
});
if (result.error) {
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
console.error('Sign-in error:', result.error.message);
}
} catch (e) {
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
console.error('Sign-in failed:', e);
}
};
// ── Email sign-in ──────────────────────────────────────────────────────────
window.signInWithEmail = async function() {
var email = document.getElementById('acct-email-input').value.trim();
var pass = document.getElementById('acct-pass-input').value;
var msg = document.getElementById('acct-email-msg');
var signinBtn = document.getElementById('acct-signin-btn');
if (!sb) { msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Loading... try again in a moment.'; return; }
if (!email || !pass) {
msg.style.display = 'block'; msg.style.color = '#c44';
msg.textContent = 'Please enter your email and password.'; return;
}
if (signinBtn) { signinBtn.disabled = true; signinBtn.textContent = 'Signing in...'; }
// Try sign in first, then sign up if not found
var result = await sb.auth.signInWithPassword({ email: email, password: pass });
if (result.error) {
if (result.error.message && result.error.message.toLowerCase().includes('invalid')) {
// Try sign up
var signupResult = await sb.auth.signUp({
email: email, password: pass,
options: { emailRedirectTo: window.location.origin + '/account' }
});
if (signupResult.error) {
msg.style.display = 'block'; msg.style.color = '#c44';
msg.textContent = signupResult.error.message; return;
}
msg.style.display = 'block'; msg.style.color = 'var(--navy)';
msg.textContent = 'Check your email to confirm your account.'; return;
}
if (signinBtn) { signinBtn.disabled = false; signinBtn.textContent = 'Sign in'; }
msg.style.display = 'block'; msg.style.color = '#c44';
msg.textContent = result.error.message; return;
}
window.location.reload();
};
// ── Sign-out ───────────────────────────────────────────────────────────────
window.signOut = async function() {
var btn = document.getElementById('signout-btn');
var btnTop = document.getElementById('signout-btn-top');
if (btn) { btn.disabled = true; btn.textContent = 'Signing out...'; }
if (btnTop) { btnTop.disabled = true; btnTop.textContent = 'Signing out...'; }
await sb.auth.signOut();
show('signin-section');
hide('dashboard-section');
if (btn) { btn.disabled = false; btn.textContent = 'Sign out'; }
if (btnTop) { btnTop.disabled = false; btnTop.textContent = 'Sign out'; }
};
// ── Render plan card ───────────────────────────────────────────────────────
async function renderPlanCard(row) {
var plan = (row && row.plan) ? row.plan : 'free';
var memberNum = (row && row.member_number) ? row.member_number : null;
var source = (row && row.source) ? row.source : '';
var createdAt = (row && row.created_at) ? row.created_at : null;
// Plan display name
var planNames = { 'founding': 'Founding Member', 'professional': 'Professional', 'free': 'Free' };
var planDisplay = planNames[plan] || 'Free';
// Status
var statusLabel = 'Preorder';
if (plan === 'free') statusLabel = 'Active';
setText('plan-name-el', planDisplay);
// Status badge
var statusHtml = '';
if (plan === 'founding' || plan === 'professional') {
statusHtml += '<span class=\"status-badge-preorder\" style=\"margin-top:.625rem;display:inline-flex\">' +
'<svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>' +
'Launching within 30 days</span>';
} else {
statusHtml += '<span class=\"plan-status\" style=\"margin-top:.625rem;display:inline-flex\"><span class=\"plan-status-dot\"></span>' + statusLabel + '</span>';
}
setHtml('plan-status-el', statusHtml);
// Billing note (replaces, never appends - this function may run more than once on auth state changes)
var billingNote = '';
if (plan === 'founding') {
billingNote = '<p class=\"plan-billing-note\">Lifetime &middot; Never billed again</p>';
} else if (plan === 'professional') {
billingNote = '<p class=\"plan-billing-note\">Billed monthly &middot; <button class=\"plan-billing-link\" onclick=\"window.location.href=\\\'/contact\\\'\">Cancel</button></p>';
} else {
billingNote = '<p class=\"plan-billing-note\">On the waitlist</p>';
}
setHtml('plan-billing-note-el', billingNote);
// Meta
var meta = '';
if (createdAt) {
var d = new Date(createdAt);
var dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Joined</span><span class=\"plan-meta-value\">' + dateStr + '</span></div>';
}
if (memberNum) {
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Member number</span><span class=\"plan-meta-value\">#' + memberNum + ' of 1,000</span></div>';
}
if (meta) {
setHtml('plan-meta-el', meta);
}
// Founding badge - always show for founding members, with member number if assigned
if (plan === 'founding') {
var badgeSection = document.getElementById('badge-section');
var badgeContainer = document.getElementById('badge-html-container');
if (badgeSection) badgeSection.style.display = '';
var badgeN = memberNum || 0;
fetch('/api/founding-badge?n=' + badgeN)
.then(function(r) { return r.text(); })
.then(function(html) {
if (badgeContainer) badgeContainer.innerHTML = html;
})
.catch(function() {});
}
// Show roadmap for founding members
var roadmapSection = document.getElementById('roadmap-section');
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
// Family section
if (plan === 'founding') {
document.getElementById('family-section').style.display = 'block';
var session = await sb.auth.getSession();
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
if (userEmail) loadFamilyMembers(userEmail);
}
}
// ── Family plan ────────────────────────────────────────────────────────────
async function loadFamilyMembers(parentEmail) {
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
var members = await r.json();
var list = document.getElementById('family-list');
if (!list) return;
if (!members || !members.length) {
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
return;
}
list.innerHTML = members.map(function(m) {
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">' +
'<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>' +
'<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>' +
'<button onclick=\"removeFamilyMember(\\\'' + m.child_email + '\\\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>' +
'</div>';
}).join('');
}
window.addFamilyMember = async function() {
var email = document.getElementById('child-email').value.trim();
var year = document.getElementById('child-dob-year').value;
var attest = document.getElementById('family-attest').checked;
var msg = document.getElementById('family-msg');
if (!email || !year || !attest) {
msg.style.display = 'block'; msg.style.color = '#c44';
msg.textContent = 'Please fill in all fields and confirm the attestation.'; return;
}
if (parseInt(year) < 2008) {
msg.style.display = 'block'; msg.style.color = '#c44';
msg.textContent = 'Child must be under 18. Birth year must be 2008 or later.'; return;
}
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
var r = await fetch('/api/family/invite', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
});
var d = await r.json();
msg.style.display = 'block';
if (d.ok) {
msg.style.color = 'var(--navy)';
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
document.getElementById('child-email').value = '';
document.getElementById('child-dob-year').value = '';
document.getElementById('family-attest').checked = false;
loadFamilyMembers(parentEmail);
} else {
msg.style.color = '#c44';
msg.textContent = d.error || 'Something went wrong.';
}
};
window.removeFamilyMember = async function(childEmail) {
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
await fetch('/api/family/remove', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
});
loadFamilyMembers(parentEmail);
};
// ── Render user info ───────────────────────────────────────────────────────
function renderUserChip(user) {
var email = user.email || '';
var avatarEl = document.getElementById('user-avatar-el');
var emailEl = document.getElementById('user-email-el');
var headerEmailEl = document.getElementById('acct-header-email');
if (emailEl) emailEl.textContent = email;
if (headerEmailEl) headerEmailEl.textContent = email;
var avatarUrl = user.user_metadata && user.user_metadata.avatar_url;
if (avatarEl) {
if (avatarUrl) {
avatarEl.innerHTML = '<img src=\"' + avatarUrl + '\" alt=\"\" referrerpolicy=\"no-referrer\">';
} else {
// Initials fallback
var initial = email ? email.charAt(0).toUpperCase() : '?';
avatarEl.textContent = initial;
}
}
}
// ── Load waitlist data ─────────────────────────────────────────────────────
async function loadWaitlistData(email) {
try {
var sess = await sb.auth.getSession();
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
if (!token) { showNoPlan(); return; }
var r = await fetch('/api/my-plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ access_token: token })
});
var row = await r.json();
if (!row || !row.plan) { showNoPlan(); return; }
renderPlanCard(row);
} catch (e) {
showNoPlan();
}
}
// ── No plan — show pricing ─────────────────────────────────────────────────
function showNoPlan() {
var el = document.getElementById('plan-card');
if (!el) return;
el.innerHTML = '<div class=\"card-label\">Your plan</div>' +
'<p style=\"font-family:var(--body);font-weight:500;font-size:1.125rem;color:var(--t1);margin-bottom:.75rem\">No active plan</p>' +
'<p style=\"font-family:var(--body);font-weight:300;font-size:.9rem;color:var(--t2);line-height:1.7;margin-bottom:1.5rem\">You have an account but no plan selected yet. Pick one below to preorder.</p>' +
'<div style=\"display:flex;gap:1rem;flex-wrap:wrap\">' +
'<a href=\"/checkout?plan=founding\" class=\"btn-primary\" style=\"padding:.75rem 1.5rem\">Founding Member - $199 &#8594;</a>' +
'<a href=\"/checkout?plan=professional\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Professional - $19/mo</a>' +
'<a href=\"/checkout?plan=free\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Free tier</a>' +
'</div>';
}
// ── Show dashboard ─────────────────────────────────────────────────────────
function showDashboard(user) {
hide('signin-section');
show('dashboard-section');
renderUserChip(user);
loadWaitlistData(user.email);
}
// ── Init ───────────────────────────────────────────────────────────────────
async function init() {
var result = await sb.auth.getSession();
var session = result.data && result.data.session;
if (session && session.user) {
showDashboard(session.user);
} else {
show('signin-section');
hide('dashboard-section');
}
// Listen for auth changes (e.g. OAuth redirect return)
sb.auth.onAuthStateChange(function(event, session) {
if (session && session.user) {
showDashboard(session.user);
} else {
show('signin-section');
hide('dashboard-section');
}
});
}
init();
})();
</script>
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";</script><script src=\"/assets/js/6dafc1586705.js\" defer></script>
</body>
</html>"
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function a0_0x1591(){var _0x10c289=['BMf2lwHHBwj1CMDLCG','ywrK','C2v0qxr0CMLIDxrL','CxvLCNLtzwXLy3rVCKfSBa','zMfSC2u','ywrKrxzLBNrmAxn0zw5LCG','mJu4oduXn3rMq0LlsG','B3bLBG','CxvLCNLtzwXLy3rVCG','y2XPy2S','zM9YrwfJAa','yxjPys1LEhbHBMrLza','Dhj1zq','A2v5zg93BG','y2XHC3nmAxn0','Dg9Nz2XL','C3rVCfbYB3bHz2f0Aw9U','mtaXodi1ngX0EfLdDG','odi3nJvNA01dEwy','n05UrhjgAa','ndq2odmXrhrPu1bs','y29UDgfPBNm','lM5HDI1KCM9Wzg93BI1PDgvT','oe9gq1H5vW','ntmXmtCXmfbmCw9Krq','DgfYz2v0','ota5nJiYqujQBwvL','BMf2','mta0reHcyu1g','CMvTB3zL','mtiZnZmXngP2AejQuq','z2v0rwXLBwvUDej5swq','CMvZAxPL','A2v5'];a0_0x1591=function(){return _0x10c289;};return a0_0x1591();}function a0_0x1c65(_0xb184c1,_0x57ab9a){_0xb184c1=_0xb184c1-0x1d0;var _0x159170=a0_0x1591();var _0x1c65a5=_0x159170[_0xb184c1];if(a0_0x1c65['WtKexP']===undefined){var _0xe40532=function(_0x40a838){var _0x2d9cb5='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x170067='',_0xe74606='';for(var _0x4a7359=0x0,_0x30df79,_0x3883ed,_0x6ecaab=0x0;_0x3883ed=_0x40a838['charAt'](_0x6ecaab++);~_0x3883ed&&(_0x30df79=_0x4a7359%0x4?_0x30df79*0x40+_0x3883ed:_0x3883ed,_0x4a7359++%0x4)?_0x170067+=String['fromCharCode'](0xff&_0x30df79>>(-0x2*_0x4a7359&0x6)):0x0){_0x3883ed=_0x2d9cb5['indexOf'](_0x3883ed);}for(var _0x907d2c=0x0,_0x3e504b=_0x170067['length'];_0x907d2c<_0x3e504b;_0x907d2c++){_0xe74606+='%'+('00'+_0x170067['charCodeAt'](_0x907d2c)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0xe74606);};a0_0x1c65['MwOGxL']=_0xe40532,a0_0x1c65['ZDZTWG']={},a0_0x1c65['WtKexP']=!![];}var _0x44174f=_0x159170[0x0],_0x518b15=_0xb184c1+_0x44174f,_0x173640=a0_0x1c65['ZDZTWG'][_0x518b15];return!_0x173640?(_0x1c65a5=a0_0x1c65['MwOGxL'](_0x1c65a5),a0_0x1c65['ZDZTWG'][_0x518b15]=_0x1c65a5):_0x1c65a5=_0x173640,_0x1c65a5;}(function(_0x37e014,_0x50cb04){var _0x44feb2=a0_0x1c65,_0x3735d2=_0x37e014();while(!![]){try{var _0x461de4=parseInt(_0x44feb2(0x1e5))/0x1+parseInt(_0x44feb2(0x1eb))/0x2+-parseInt(_0x44feb2(0x1ef))/0x3+parseInt(_0x44feb2(0x1ed))/0x4*(parseInt(_0x44feb2(0x1e3))/0x5)+parseInt(_0x44feb2(0x1e2))/0x6*(parseInt(_0x44feb2(0x1e4))/0x7)+parseInt(_0x44feb2(0x1e8))/0x8*(-parseInt(_0x44feb2(0x1d7))/0x9)+-parseInt(_0x44feb2(0x1e9))/0xa;if(_0x461de4===_0x50cb04)break;else _0x3735d2['push'](_0x3735d2['shift']());}catch(_0x5902f6){_0x3735d2['push'](_0x3735d2['shift']());}}}(a0_0x1591,0x420ab),!(function(){var _0x547ce7=a0_0x1c65,_0x170067=document['getElementById'](_0x547ce7(0x1d1)),_0xe74606=document[_0x547ce7(0x1f0)]('nav-mobile'),_0x4a7359=document[_0x547ce7(0x1f0)](_0x547ce7(0x1ec));if(_0x170067&&_0xe74606){_0x170067[_0x547ce7(0x1d6)](_0x547ce7(0x1da),function(_0x907d2c){var _0x153658=_0x547ce7;_0x907d2c[_0x153658(0x1e1)](),_0xe74606[_0x153658(0x1df)][_0x153658(0x1e6)](_0x153658(0x1d8))?_0x6ecaab():(_0xe74606[_0x153658(0x1df)][_0x153658(0x1d2)](_0x153658(0x1d8)),_0x170067[_0x153658(0x1d3)](_0x153658(0x1dc),_0x153658(0x1dd)));});var _0x30df79=document[_0x547ce7(0x1d9)]('.nav-dropdown-btn'),_0x3883ed=document[_0x547ce7(0x1d9)]('.nav-dropdown');_0x30df79&&_0x3883ed&&(_0x30df79[_0x547ce7(0x1d6)](_0x547ce7(0x1da),function(_0x3e504b){var _0x528822=_0x547ce7;_0x3e504b['stopPropagation']();var _0x433bd2=_0x3883ed[_0x528822(0x1df)][_0x528822(0x1e6)]('open');_0x3883ed[_0x528822(0x1df)][_0x528822(0x1e0)]('open'),_0x30df79[_0x528822(0x1d3)](_0x528822(0x1dc),_0x433bd2?_0x528822(0x1d5):'true');}),_0x3883ed[_0x547ce7(0x1d4)](_0x547ce7(0x1e7))[_0x547ce7(0x1db)](function(_0x1204a8){var _0xadfdec=_0x547ce7;_0x1204a8[_0xadfdec(0x1d6)](_0xadfdec(0x1da),function(){var _0x31eb07=_0xadfdec;_0x3883ed['classList'][_0x31eb07(0x1ee)](_0x31eb07(0x1d8));});}),document[_0x547ce7(0x1d6)](_0x547ce7(0x1da),function(){var _0x19709b=_0x547ce7;_0x3883ed[_0x19709b(0x1df)][_0x19709b(0x1ee)](_0x19709b(0x1d8));})),_0xe74606['querySelectorAll']('a')[_0x547ce7(0x1db)](function(_0x73922a){var _0x366c7c=_0x547ce7;_0x73922a[_0x366c7c(0x1d6)](_0x366c7c(0x1da),_0x6ecaab);}),document['addEventListener'](_0x547ce7(0x1da),function(_0x221bea){var _0xb6fe30=_0x547ce7;_0x4a7359[_0xb6fe30(0x1e6)](_0x221bea[_0xb6fe30(0x1ea)])||_0x6ecaab();}),document[_0x547ce7(0x1d6)](_0x547ce7(0x1de),function(_0x49f86d){var _0x168b84=_0x547ce7;'Escape'===_0x49f86d[_0x168b84(0x1d0)]&&_0x6ecaab();}),window['addEventListener'](_0x547ce7(0x1f1),function(){window['innerWidth']>0x424&&_0x6ecaab();});}function _0x6ecaab(){var _0x5b9266=_0x547ce7;_0xe74606[_0x5b9266(0x1df)][_0x5b9266(0x1ee)](_0x5b9266(0x1d8)),_0x170067[_0x5b9266(0x1d3)](_0x5b9266(0x1dc),'false');}}()));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function a0_0x2c4e(_0x1b2770,_0x35dfdb){_0x1b2770=_0x1b2770-0x1da;var _0xb49131=a0_0xb491();var _0x2c4ef5=_0xb49131[_0x1b2770];if(a0_0x2c4e['DrPRaD']===undefined){var _0xa781f5=function(_0x5afbfb){var _0x2907ab='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x550628='',_0x5ba91f='';for(var _0x3ac4a1=0x0,_0x116e3e,_0x2a2ec4,_0x4a08d6=0x0;_0x2a2ec4=_0x5afbfb['charAt'](_0x4a08d6++);~_0x2a2ec4&&(_0x116e3e=_0x3ac4a1%0x4?_0x116e3e*0x40+_0x2a2ec4:_0x2a2ec4,_0x3ac4a1++%0x4)?_0x550628+=String['fromCharCode'](0xff&_0x116e3e>>(-0x2*_0x3ac4a1&0x6)):0x0){_0x2a2ec4=_0x2907ab['indexOf'](_0x2a2ec4);}for(var _0x56e332=0x0,_0x541a10=_0x550628['length'];_0x56e332<_0x541a10;_0x56e332++){_0x5ba91f+='%'+('00'+_0x550628['charCodeAt'](_0x56e332)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x5ba91f);};a0_0x2c4e['zlCeOW']=_0xa781f5,a0_0x2c4e['KwBFPV']={},a0_0x2c4e['DrPRaD']=!![];}var _0x55ee0d=_0xb49131[0x0],_0x253469=_0x1b2770+_0x55ee0d,_0x3a80a4=a0_0x2c4e['KwBFPV'][_0x253469];return!_0x3a80a4?(_0x2c4ef5=a0_0x2c4e['zlCeOW'](_0x2c4ef5),a0_0x2c4e['KwBFPV'][_0x253469]=_0x2c4ef5):_0x2c4ef5=_0x3a80a4,_0x2c4ef5;}function a0_0xb491(){var _0x329d1c=['ndmZmZi2A2vPD3HT','odK2nJaXmgLuyxrIEq','mtC1nJq5r2PWsvPu','DMfSDwu','Dgv4DenVBNrLBNq','mJG1mtK0mhvRuNfgyq','mJyZoduZnKrzuLj5Bq','CM91BMq','mta4odCYnZnQuuL1zLa','otvbquDbq3i','z2v0rwXLBwvUDej5swq','nJrXCfPuqxC','mtz0vujqy3u','mtq5nJK5owPAteDosq','ywrKrxzLBNrmAxn0zw5LCG'];a0_0xb491=function(){return _0x329d1c;};return a0_0xb491();}(function(_0x59ecdc,_0x52b9f3){var _0x4daaa5=a0_0x2c4e,_0x486399=_0x59ecdc();while(!![]){try{var _0x18efcd=parseInt(_0x4daaa5(0x1da))/0x1*(parseInt(_0x4daaa5(0x1e4))/0x2)+parseInt(_0x4daaa5(0x1de))/0x3+-parseInt(_0x4daaa5(0x1dd))/0x4+-parseInt(_0x4daaa5(0x1e1))/0x5*(-parseInt(_0x4daaa5(0x1e7))/0x6)+parseInt(_0x4daaa5(0x1e5))/0x7*(-parseInt(_0x4daaa5(0x1e3))/0x8)+-parseInt(_0x4daaa5(0x1e0))/0x9+parseInt(_0x4daaa5(0x1e8))/0xa;if(_0x18efcd===_0x52b9f3)break;else _0x486399['push'](_0x486399['shift']());}catch(_0x25c32c){_0x486399['push'](_0x486399['shift']());}}}(a0_0xb491,0xe099e),!(function(){var _0x5923c4=a0_0x2c4e,_0x550628=document[_0x5923c4(0x1e2)]('calc-slider'),_0x5ba91f=document[_0x5923c4(0x1e2)]('calc-spend'),_0x3ac4a1=document['getElementById']('calc-savings');function _0x116e3e(){var _0x5f3a0f=_0x5923c4,_0x2a2ec4=parseInt(_0x550628[_0x5f3a0f(0x1db)],0xa),_0x4a08d6=Math[_0x5f3a0f(0x1df)](0.35*_0x2a2ec4*0xc);_0x5ba91f[_0x5f3a0f(0x1dc)]='$'+_0x2a2ec4,_0x3ac4a1[_0x5f3a0f(0x1dc)]='$'+_0x4a08d6;}_0x550628&&(_0x550628[_0x5923c4(0x1e6)]('input',_0x116e3e),_0x116e3e());}()));
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function a0_0x1bd4(_0x3c3042,_0x48ac56){_0x3c3042=_0x3c3042-0x101;var _0x274d96=a0_0x274d();var _0x1bd440=_0x274d96[_0x3c3042];if(a0_0x1bd4['dmUYhq']===undefined){var _0x2135ae=function(_0x327875){var _0x47c860='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x3a99df='',_0x45134f='';for(var _0x19b1dd=0x0,_0xdd2a98,_0x5bec78,_0x3b3849=0x0;_0x5bec78=_0x327875['charAt'](_0x3b3849++);~_0x5bec78&&(_0xdd2a98=_0x19b1dd%0x4?_0xdd2a98*0x40+_0x5bec78:_0x5bec78,_0x19b1dd++%0x4)?_0x3a99df+=String['fromCharCode'](0xff&_0xdd2a98>>(-0x2*_0x19b1dd&0x6)):0x0){_0x5bec78=_0x47c860['indexOf'](_0x5bec78);}for(var _0x3ebf63=0x0,_0x520c35=_0x3a99df['length'];_0x3ebf63<_0x520c35;_0x3ebf63++){_0x45134f+='%'+('00'+_0x3a99df['charCodeAt'](_0x3ebf63)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x45134f);};a0_0x1bd4['CEZewz']=_0x2135ae,a0_0x1bd4['PvzCOb']={},a0_0x1bd4['dmUYhq']=!![];}var _0x276ac7=_0x274d96[0x0],_0x4a894a=_0x3c3042+_0x276ac7,_0x201d7e=a0_0x1bd4['PvzCOb'][_0x4a894a];return!_0x201d7e?(_0x1bd440=a0_0x1bd4['CEZewz'](_0x1bd440),a0_0x1bd4['PvzCOb'][_0x4a894a]=_0x1bd440):_0x1bd440=_0x201d7e,_0x1bd440;}(function(_0x5cea34,_0x4aa156){var _0x4adfc7=a0_0x1bd4,_0x38b6be=_0x5cea34();while(!![]){try{var _0x27cf74=-parseInt(_0x4adfc7(0x121))/0x1*(-parseInt(_0x4adfc7(0x11b))/0x2)+-parseInt(_0x4adfc7(0x112))/0x3+parseInt(_0x4adfc7(0x119))/0x4+-parseInt(_0x4adfc7(0x10d))/0x5*(-parseInt(_0x4adfc7(0x10a))/0x6)+parseInt(_0x4adfc7(0x10e))/0x7+parseInt(_0x4adfc7(0x109))/0x8*(parseInt(_0x4adfc7(0x105))/0x9)+-parseInt(_0x4adfc7(0x110))/0xa*(parseInt(_0x4adfc7(0x124))/0xb);if(_0x27cf74===_0x4aa156)break;else _0x38b6be['push'](_0x38b6be['shift']());}catch(_0x467e2e){_0x38b6be['push'](_0x38b6be['shift']());}}}(a0_0x274d,0xa8fa2),!(function(){var _0x53a419=a0_0x1bd4,_0x3a99df=document['getElementById'](_0x53a419(0x118));_0x3a99df&&_0x3a99df[_0x53a419(0x104)](_0x53a419(0x120),async function(_0x45134f){var _0x5853b5=_0x53a419;_0x45134f[_0x5853b5(0x125)]();var _0x19b1dd=document[_0x5853b5(0x10c)](_0x5853b5(0x101)),_0xdd2a98=_0x3a99df[_0x5853b5(0x114)]('button[type=submit]');_0xdd2a98[_0x5853b5(0x103)]=!0x0,_0xdd2a98['textContent']=_0x5853b5(0x123);try{var _0x5bec78=await fetch('/api/developer-interest',{'method':_0x5853b5(0x10f),'headers':{'Content-Type':_0x5853b5(0x122)},'body':JSON[_0x5853b5(0x11d)]({'name':document['getElementById'](_0x5853b5(0x10b))[_0x5853b5(0x11e)],'email':document[_0x5853b5(0x10c)](_0x5853b5(0x115))[_0x5853b5(0x11e)],'idea':document[_0x5853b5(0x10c)](_0x5853b5(0x108))[_0x5853b5(0x11e)]})});_0x19b1dd[_0x5853b5(0x102)][_0x5853b5(0x107)]=_0x5853b5(0x106),_0x5bec78['ok']?(_0x19b1dd[_0x5853b5(0x111)]='Got\x20it.\x20Will\x20review\x20it\x20personally\x20and\x20reach\x20out.',_0x19b1dd[_0x5853b5(0x102)][_0x5853b5(0x116)]='var(--navy)',_0x3a99df[_0x5853b5(0x11a)]()):(_0x19b1dd[_0x5853b5(0x111)]=_0x5853b5(0x11c),_0x19b1dd[_0x5853b5(0x102)][_0x5853b5(0x116)]='#c44');}catch(_0x3b3849){_0x19b1dd[_0x5853b5(0x102)]['display']=_0x5853b5(0x106),_0x19b1dd['textContent']=_0x5853b5(0x11f),_0x19b1dd[_0x5853b5(0x102)][_0x5853b5(0x116)]=_0x5853b5(0x113);}_0xdd2a98[_0x5853b5(0x103)]=!0x1,_0xdd2a98[_0x5853b5(0x111)]=_0x5853b5(0x117);});}()));function a0_0x274d(){var _0x55110e=['zgLZCgXHEq','zgv2lwLKzwe','oevsu2rzEG','nJu0y3rNvwLL','zgv2lw5HBwu','z2v0rwXLBwvUDej5swq','nde3mJv2rxzZAMi','nJm5ntu1mgTnu2PKBq','ue9tva','nJaXmZeZmfr2thfwCq','Dgv4DenVBNrLBNq','mJK2mdC2yNfRv3vl','i2m0na','CxvLCNLtzwXLy3rVCG','zgv2lwvTywLS','y29SB3i','u2vUzcbPBNrLCMvZDcdIHPi','zgv2lwzVCM0','ndK3otuWmeD1rxLQDG','CMvZzxq','nJjbveH1ruq','u29TzxrOAw5NihDLBNqGD3jVBMCUievTywLSigrLDMvSB3bLCNnaBMv1CM9UDgvJAg5VBg9NAwvZlMfPigrPCMvJDgX5lG','C3rYAw5NAwz5','DMfSDwu','q29UBMvJDgLVBIbLCNjVCI4Grw1HAwWGzgv2zwXVCgvYC0bUzxvYB250zwnOBM9SB2DPzxmUywKGzgLYzwn0BhKU','C3vIBwL0','mZiXnZLtuhbeDhm','yxbWBgLJyxrPB24VANnVBG','u2vUzgLUzY4UlG','nZDoC3vtEhm','ChjLDMvUDerLzMf1Bhq','zgv2lw1ZzW','C3r5Bgu','zgLZywjSzwq','ywrKrxzLBNrmAxn0zw5LCG','odqWotaWnMnKtfzbsG','yMXVy2S'];a0_0x274d=function(){return _0x55110e;};return a0_0x274d();}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+94
View File
@@ -0,0 +1,94 @@
{
"generated_by": "scripts/extract-js.py",
"count": 11,
"entries": [
{
"file": "account.el",
"hash": "6dafc1586705",
"asset": "/assets/js/6dafc1586705.js",
"size": 18055,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "checkout.el",
"hash": "db455e1671dd",
"asset": "/assets/js/db455e1671dd.js",
"size": 9701,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "checkout.el",
"hash": "e708dcbb3e7a",
"asset": "/assets/js/e708dcbb3e7a.js",
"size": 10802,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "enterprise.el",
"hash": "67c990f787eb",
"asset": "/assets/js/67c990f787eb.js",
"size": 5149,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "environmental.el",
"hash": "9bbad1ad5acb",
"asset": "/assets/js/9bbad1ad5acb.js",
"size": 2602,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "gallery.el",
"hash": "cd30551e3c3b",
"asset": "/assets/js/cd30551e3c3b.js",
"size": 6693,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "main.el",
"hash": "94727a87c328",
"asset": "/assets/js/94727a87c328.js",
"size": 5173,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "marketplace.el",
"hash": "ce12d682c9e6",
"asset": "/assets/js/ce12d682c9e6.js",
"size": 4046,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "nav.el",
"hash": "529d45d105c9",
"asset": "/assets/js/529d45d105c9.js",
"size": 4511,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "styles.el",
"hash": "407e72cd7182",
"asset": "/assets/js/407e72cd7182.js",
"size": 6430,
"interpolated": [],
"note": "carried from prior run"
},
{
"file": "styles.el",
"hash": "fc247ef45b1d",
"asset": "/assets/js/fc247ef45b1d.js",
"size": 18624,
"interpolated": [],
"note": "carried from prior run"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+2 -404
View File
@@ -485,410 +485,8 @@ fn checkout_page(plan: String, pub_key: String) -> String {
.checkout-auth-badge strong { color: var(--navy); font-weight: 500; }
</style>
<script>
// ── Supabase auth ─────────────────────────────────────────────────────────────
(function() {
var supabase;
<script src=\"/assets/js/db455e1671dd.js\" defer></script>
function initSupabase(cb) {
if (supabase) { cb(); return; }
fetch('/api/supabase-config')
.then(function(r) { return r.json(); })
.then(function(cfg) {
supabase = window.supabase.createClient(cfg.url, cfg.anon_key, {
auth: { flowType: 'implicit' }
});
cb();
})
.catch(function(err) {
console.error('Supabase init failed', err);
});
}
function showAuthMessage(msg, isError) {
var el = document.getElementById('auth-message');
el.textContent = msg;
el.style.display = 'block';
el.style.color = isError ? '#c0392b' : '#2ecc71';
}
function revealPaymentForm(user) {
// Capture Supabase user id so /api/link-customer can stamp the Stripe
// customer with metadata[supabase_user_id]. Used by /account to find
// the buyer's plan via the canonical cross-reference.
if (user && user.id) { window._neuronSupaId = user.id; }
// Hide optional auth section if it was shown
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
// Show auth badge if we have a user, hide the inline 'sign in' prompt
if (user) {
var badge = document.getElementById('auth-badge');
var name = user.user_metadata && user.user_metadata.full_name
? user.user_metadata.full_name
: user.email || '';
badge.innerHTML = '<div class=\"checkout-auth-badge\">'
+ '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M20 6L9 17l-5-5\" stroke=\"#0052A0\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'
+ 'Signed in as <strong>' + name + '</strong>'
+ '</div>';
badge.style.display = '';
var prompt = document.getElementById('signin-prompt');
if (prompt) prompt.style.display = 'none';
}
// Pre-fill email only (not name - let user enter their own)
if (user && user.email) {
var emailEl = document.getElementById('buyer-email');
if (emailEl) { emailEl.value = user.email; }
}
// Initialize Stripe Elements with this user's email (or empty if guest)
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof initStripe === 'function') initStripe(userEmail, userName);
}
// Check if already signed in on load
function checkExistingSession() {
initSupabase(function() {
supabase.auth.getUser().then(function(res) {
if (res.data && res.data.user) {
revealPaymentForm(res.data.user);
}
});
});
}
// Handle OAuth redirect callback
function handleAuthRedirect() {
initSupabase(function() {
supabase.auth.onAuthStateChange(function(event, session) {
if ((event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session && session.user) {
revealPaymentForm(session.user);
}
});
});
}
// Social sign-in
window.signInWith = function(provider) {
var btns = document.querySelectorAll('.checkout-social-btn');
btns.forEach(function(b) { b.disabled = true; });
initSupabase(function() {
supabase.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo: window.location.href
}
}).then(function(result) {
if (result.error) {
showAuthMessage(result.error.message || 'Sign-in failed. Please try again.', true);
btns.forEach(function(b) { b.disabled = false; });
}
// On success, browser redirects to OAuth provider - no further action needed here.
});
});
};
// Email signup
window.signUpWithEmail = function() {
var email = document.getElementById('auth-email').value.trim();
var password = document.getElementById('auth-password').value;
if (!email || !password) { showAuthMessage('Please enter your email and a password.', true); return; }
if (password.length < 8) { showAuthMessage('Password must be at least 8 characters.', true); return; }
initSupabase(function() {
supabase.auth.signUp({ email: email, password: password }).then(function(result) {
if (result.error) { showAuthMessage(result.error.message, true); return; }
if (result.data && result.data.session) {
revealPaymentForm(result.data.session.user);
} else {
showAuthMessage('Check your email to confirm your account, then come back to complete your purchase.', false);
}
});
});
};
// Email sign-in (existing account)
window.showSignIn = function() {
var form = document.getElementById('email-auth-form');
var btn = form.querySelector('.checkout-email-btn');
var hint = form.querySelector('.checkout-auth-hint');
btn.textContent = 'Sign in →';
btn.onclick = signInWithEmail;
/* hint replaced with DOM manipulation below */
};
window.showSignUp = function() {
var form = document.getElementById('email-auth-form');
var btn = form.querySelector('.checkout-email-btn');
var hint = form.querySelector('.checkout-auth-hint');
btn.textContent = 'Create account →';
btn.onclick = signUpWithEmail;
/* hint replaced with DOM manipulation below */
};
window.signInWithEmail = function() {
var email = document.getElementById('auth-email').value.trim();
var password = document.getElementById('auth-password').value;
if (!email || !password) { showAuthMessage('Please enter your email and password.', true); return; }
initSupabase(function() {
supabase.auth.signInWithPassword({ email: email, password: password }).then(function(result) {
if (result.error) { showAuthMessage(result.error.message, true); return; }
revealPaymentForm(result.data.session.user);
});
});
};
window.resetPassword = function() {
var email = document.getElementById('auth-email').value.trim();
if (!email) { showAuthMessage('Enter your email address above first.', true); return; }
initSupabase(function() {
supabase.auth.resetPasswordForEmail(email, {
redirectTo: window.location.origin + '/checkout?plan=' + (new URLSearchParams(window.location.search).get('plan') || 'professional')
}).then(function(result) {
if (result.error) { showAuthMessage(result.error.message, true); }
else { showAuthMessage('Password reset email sent. Check your inbox.', false); }
});
});
};
// Init
handleAuthRedirect();
checkExistingSession();
})();
</script>
<script>
(function() {
var PLAN = '" + plan + "';
var STRIPE_PK = '" + pub_key + "';
var stripe, elements;
// Wait for Stripe.js to load
function waitForStripe(cb) {
if (window.Stripe) { cb(); return; }
setTimeout(function() { waitForStripe(cb); }, 50);
}
function showMessage(msg) {
var el = document.getElementById('payment-message');
el.textContent = msg;
el.style.display = 'block';
}
function setLoading(loading) {
var btn = document.getElementById('submit-btn');
var label = document.getElementById('submit-label');
var spinner = document.getElementById('submit-spinner');
btn.disabled = loading;
label.style.display = loading ? 'none' : '';
spinner.style.display = loading ? '' : 'none';
}
// Mode flag: 'payment' (charge now) or 'setup' (save card, charge later).
// The submit handler reads this to choose stripe.confirmPayment vs
// stripe.confirmSetup. SOURCE OF TRUTH for whether the buyer is going to
// be charged at submit time. If the radio toggles, we re-fetch a new
// client_secret of the right type and re-mount the Element.
window._neuronMode = 'payment';
var paymentEl = null;
function appearance() {
return {
theme: 'flat',
variables: {
colorPrimary: '#0052A0',
colorBackground: '#ffffff',
colorText: '#1A1A2E',
colorDanger: '#c0392b',
colorTextPlaceholder:'#9B9BAD',
borderRadius: '0px',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontSizeBase: '15px',
fontWeightNormal: '300',
spacingUnit: '4px'
},
rules: {
'.Input': { border: '1px solid rgba(0,82,160,.22)', boxShadow: 'none', padding: '10px 14px' },
'.Input:focus': { border: '1px solid rgba(0,82,160,.6)', boxShadow: '0 0 0 3px rgba(0,82,160,.08)', outline: 'none' },
'.Label': { fontSize: '11px', fontWeight: '500', letterSpacing: '.06em', textTransform: 'uppercase', color: '#6B6B7E', marginBottom: '6px' },
'.Tab': { border: '1px solid rgba(0,82,160,.18)', boxShadow: 'none' },
'.Tab--selected': { border: '1px solid rgba(0,82,160,.5)', boxShadow: '0 0 0 2px rgba(0,82,160,.12)' },
'.Error': { color: '#c0392b' }
}
};
}
function currentTiming() {
var later = document.getElementById('timing-later');
return (later && later.checked) ? 'later' : 'now';
}
function fetchAndMount() {
var submitBtn = document.getElementById('submit-btn');
if (submitBtn) { submitBtn.disabled = true; }
if (paymentEl) {
try { paymentEl.unmount(); } catch(e) {}
paymentEl = null;
}
var loadEl = document.querySelector('.checkout-element-loading');
if (!loadEl) {
var hostEl = document.getElementById('payment-element');
if (hostEl) {
var d = document.createElement('div');
d.className = 'checkout-element-loading';
d.textContent = 'Loading payment form…';
hostEl.appendChild(d);
}
}
var timing = currentTiming();
return fetch('/api/payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan: PLAN, timing: timing })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error === 'sold_out') {
showMessage('All 1,000 Founding Member spots have been claimed. Thank you for your interest - please consider the Professional plan.');
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Sold out'; }
return;
}
if (!data.client_secret) {
showMessage('Unable to initialise payment. Please try again.');
return;
}
window._neuronMode = data.setup_mode ? 'setup' : 'payment';
window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : '');
// Update the submit button label so users know what will happen.
var submitLabel = document.getElementById('submit-label');
if (submitLabel) {
submitLabel.textContent = window._neuronMode === 'setup'
? 'Save my card - no charge today →'
: 'Complete purchase →';
}
waitForStripe(function() {
if (!stripe) { stripe = Stripe(STRIPE_PK); }
elements = stripe.elements({ clientSecret: data.client_secret, appearance: appearance() });
paymentEl = elements.create('payment', {
fields: { billingDetails: { name: 'never', email: 'never' } }
});
paymentEl.mount('#payment-element');
paymentEl.on('ready', function() {
var ld = document.querySelector('.checkout-element-loading');
if (ld) ld.remove();
if (submitBtn) submitBtn.disabled = false;
});
});
})
.catch(function() {
showMessage('Unable to connect. Please check your connection and try again.');
});
}
// Initial mount. PLAN==professional shows a timing radio; toggling it
// re-fetches the right Intent so the buyer's choice is honored.
fetchAndMount();
var tNow = document.getElementById('timing-now');
var tLater = document.getElementById('timing-later');
if (tNow) tNow.addEventListener('change', fetchAndMount);
if (tLater) tLater.addEventListener('change', fetchAndMount);
// Submit
document.getElementById('payment-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!stripe || !elements) return;
// Founding Member attestation gate
var attestCb = document.getElementById('founding-attest-cb');
if (attestCb && !attestCb.checked) {
var warn = document.getElementById('attest-warn');
if (warn) warn.style.display = 'block';
attestCb.closest('label').scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
var name = document.getElementById('buyer-name').value.trim();
var email = document.getElementById('buyer-email').value.trim();
if (!name || !email) {
showMessage('Please enter your name and email.');
return;
}
// Save founding member attestation before charging - independent audit record
if (attestCb) {
try {
await fetch('/api/attest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan: PLAN,
name: name,
email: email,
timestamp: new Date().toISOString(),
attestation: 'I am joining as a genuine early user, not to extract proprietary information about Neuron technology, architecture, or roadmap. I will engage in good faith. I understand that if this is not my intent, a different plan is a better fit.',
user_agent: navigator.userAgent
})
});
} catch(e) {
// Non-blocking - attestation log failure does not stop payment
console.warn('attestation log failed', e);
}
}
setLoading(true);
document.getElementById('payment-message').style.display = 'none';
if (window._neuronPiId) {
try {
await fetch('/api/link-customer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pi_id: window._neuronPiId,
email: email,
name: name,
plan: PLAN,
timing: currentTiming(),
mode: window._neuronMode || 'payment',
supabase_user_id: window._neuronSupaId || ''
})
});
} catch(e) { /* non-blocking */ }
}
// Setup mode: save card, do not charge. Use stripe.confirmSetup.
// Payment mode: charge now via stripe.confirmPayment.
var confirmParams = {
return_url: window.location.origin + '/account?welcome=1',
payment_method_data: { billing_details: { name: name, email: email } }
};
var result;
if (window._neuronMode === 'setup') {
result = await stripe.confirmSetup({
elements: elements,
confirmParams: confirmParams
});
} else {
confirmParams.receipt_email = email;
result = await stripe.confirmPayment({
elements: elements,
confirmParams: confirmParams
});
}
if (result.error) {
showMessage(result.error.message || (window._neuronMode === 'setup'
? 'Could not save your card. Please try again.'
: 'Payment failed. Please try again.'));
setLoading(false);
}
// On success, Stripe redirects to return_url automatically.
});
})();
</script>
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan=\"" + plan + "\";window.NEURON_CFG.pub_key=\"" + pub_key + "\";</script><script src=\"/assets/js/e708dcbb3e7a.js\" defer></script>
"
}
+1 -60
View File
@@ -182,65 +182,6 @@ fn enterprise() -> String {
}
</style>
<script>
(function() {
var form = document.getElementById('enterprise-form');
var submitBtn = document.getElementById('ent-submit');
var successDiv = document.getElementById('enterprise-success');
var errorDiv = document.getElementById('ent-form-error');
if (!form) return;
window.entHeadcountChange = function(val) {
document.getElementById('ent-filter-msg-secondary').style.display = val === 'secondary' ? 'block' : 'none';
document.getElementById('ent-filter-msg-yes').style.display = val === 'yes' ? 'block' : 'none';
submitBtn.disabled = val === 'yes';
submitBtn.style.opacity = val === 'yes' ? '0.35' : '1';
submitBtn.style.cursor = val === 'yes' ? 'not-allowed' : 'pointer';
};
form.addEventListener('submit', function(e) {
e.preventDefault();
var headcount = document.getElementById('ent-headcount').value;
if (headcount === 'yes') {
document.getElementById('ent-filter-msg-yes').style.display = 'block';
return;
}
var name = document.getElementById('ent-name').value.trim();
var email = document.getElementById('ent-email').value.trim();
var company = document.getElementById('ent-company').value.trim();
var size = document.getElementById('ent-size').value;
var useCase = document.getElementById('ent-use').value.trim();
if (!name || !email || !company || !size || !useCase || !headcount) {
errorDiv.textContent = 'Please fill out all fields.';
errorDiv.style.display = 'block';
return;
}
errorDiv.style.display = 'none';
submitBtn.textContent = 'Sending…';
submitBtn.disabled = true;
fetch('/api/enterprise-inquiry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, email: email, company: company, size: size, use_case: useCase, headcount: headcount })
})
.then(function(r) { return r.json(); })
.then(function(data) {
form.style.display = 'none';
successDiv.style.display = 'block';
})
.catch(function() {
submitBtn.textContent = 'Send inquiry →';
submitBtn.disabled = false;
errorDiv.textContent = 'Something went wrong. Email enterprise@neurontechnologies.ai directly.';
errorDiv.style.display = 'block';
});
});
})();
</script>
<script src=\"/assets/js/67c990f787eb.js\" defer></script>
"
}
+1 -16
View File
@@ -44,22 +44,7 @@ fn environmental() -> String {
<p style=\"font-family:var(--body);font-size:0.75rem;color:var(--t3)\">Based on estimated token reduction applied to your monthly spend.</p>
</div>
<script>
(function() {
var slider = document.getElementById('calc-slider');
var spendEl = document.getElementById('calc-spend');
var savingsEl = document.getElementById('calc-savings');
if (!slider) return;
function update() {
var monthly = parseInt(slider.value, 10);
var annual = Math.round(monthly * 0.35 * 12);
spendEl.textContent = '$' + monthly;
savingsEl.textContent = '$' + annual;
}
slider.addEventListener('input', update);
update();
})();
</script>
<script src=\"/assets/js/9bbad1ad5acb.js\" defer></script>
</div>
<div style=\"display:flex;flex-direction:column;gap:1.5rem;padding-top:1rem\">
+1 -77
View File
@@ -185,83 +185,7 @@ body::before {
</div>
</div>
<script>
(function() {
// Nav scroll effect
var nav = document.getElementById('nav');
if (nav) window.addEventListener('scroll', function() {
nav.classList.toggle('scrolled', window.scrollY > 10);
}, { passive: true });
// Hamburger
var btn = document.getElementById('nav-hamburger');
var menu = document.getElementById('nav-mobile');
if (btn && menu) {
function navClose() { menu.classList.remove('open'); btn.setAttribute('aria-expanded','false'); }
function navOpen() { menu.classList.add('open'); btn.setAttribute('aria-expanded','true'); }
btn.addEventListener('click', function(e) { e.stopPropagation(); menu.classList.contains('open') ? navClose() : navOpen(); });
menu.querySelectorAll('a').forEach(function(a) { a.addEventListener('click', navClose); });
document.addEventListener('click', function(e) { if (!nav.contains(e.target)) navClose(); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') navClose(); });
window.addEventListener('resize', function() { if (window.innerWidth > 1060) navClose(); });
}
// Dropdown - Mission
var ddBtn = document.querySelector('.nav-dropdown-btn');
var dd = document.querySelector('.nav-dropdown');
if (ddBtn && dd) {
ddBtn.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = dd.classList.contains('open');
dd.classList.toggle('open');
ddBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
dd.querySelectorAll('.nav-dropdown-item').forEach(function(a) { a.addEventListener('click', function() { dd.classList.remove('open'); }); });
document.addEventListener('click', function() { dd.classList.remove('open'); });
}
// Search
var searchEl = document.getElementById('gal-search');
var grid = document.getElementById('gallery-grid');
var noResults = document.getElementById('no-results');
function filterCards() {
var q = (searchEl.value || '').toLowerCase().trim();
var cards = grid.querySelectorAll('.gal-card');
var visible = 0;
cards.forEach(function(c) {
var text = c.textContent.toLowerCase();
var match = !q || text.indexOf(q) !== -1;
c.classList.toggle('hidden', !match);
if (match) visible++;
});
noResults.style.display = visible === 0 && q ? 'block' : 'none';
}
if (searchEl) searchEl.addEventListener('input', filterCards);
// Sort
var currentSort = 'top';
window.setSort = function(mode, btn) {
document.querySelectorAll('.sort-btn').forEach(function(b){ b.classList.remove('active'); });
btn.classList.add('active');
currentSort = mode;
var cards = Array.from(grid.querySelectorAll('.gal-card'));
cards.sort(function(a, b) {
if (mode === 'top') {
var aV = parseInt(a.getAttribute('data-score') || '0');
var bV = parseInt(b.getAttribute('data-score') || '0');
return bV - aV;
} else {
var aT = parseInt(a.getAttribute('data-ts') || '0');
var bT = parseInt(b.getAttribute('data-ts') || '0');
return bT - aT;
}
});
cards.forEach(function(c){ grid.appendChild(c); });
};
})();
</script>
<script src=\"/assets/js/cd30551e3c3b.js\" defer></script>
</body>
</html>"
}
+1 -39
View File
@@ -132,45 +132,7 @@ fn marketplace() -> String {
</div>
</div>
<script>
(function() {
var form = document.getElementById('dev-form');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
var msg = document.getElementById('dev-msg');
var btn = form.querySelector('button[type=submit]');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
var r = await fetch('/api/developer-interest', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: document.getElementById('dev-name').value,
email: document.getElementById('dev-email').value,
idea: document.getElementById('dev-idea').value
})
});
msg.style.display = 'block';
if (r.ok) {
msg.textContent = 'Got it. Will review it personally and reach out.';
msg.style.color = 'var(--navy)';
form.reset();
} else {
msg.textContent = 'Something went wrong. Email developers@neurontechnologies.ai directly.';
msg.style.color = '#c44';
}
} catch(err) {
msg.style.display = 'block';
msg.textContent = 'Connection error. Email developers@neurontechnologies.ai directly.';
msg.style.color = '#c44';
}
btn.disabled = false;
btn.textContent = 'Send interest →';
});
})();
</script>
<script src=\"/assets/js/ce12d682c9e6.js\" defer></script>
</section>
+1 -58
View File
@@ -52,63 +52,6 @@ fn nav() -> String {
</div>
</nav>
<script>
(function() {
var btn = document.getElementById('nav-hamburger');
var menu = document.getElementById('nav-mobile');
var nav = document.getElementById('nav');
if (!btn || !menu) return;
function close() {
menu.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
function open() {
menu.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
}
function toggle() {
if (menu.classList.contains('open')) { close(); } else { open(); }
}
btn.addEventListener('click', function(e) { e.stopPropagation(); toggle(); });
// Dropdown — Mission
var ddBtn = document.querySelector('.nav-dropdown-btn');
var dd = document.querySelector('.nav-dropdown');
if (ddBtn && dd) {
ddBtn.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = dd.classList.contains('open');
dd.classList.toggle('open');
ddBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
dd.querySelectorAll('.nav-dropdown-item').forEach(function(a) {
a.addEventListener('click', function() { dd.classList.remove('open'); });
});
document.addEventListener('click', function() { dd.classList.remove('open'); });
}
// Close on any link inside mobile menu
menu.querySelectorAll('a').forEach(function(a) {
a.addEventListener('click', close);
});
// Close when clicking outside
document.addEventListener('click', function(e) {
if (!nav.contains(e.target)) { close(); }
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { close(); }
});
// Close if viewport grows past breakpoint
window.addEventListener('resize', function() {
if (window.innerWidth > 1060) { close(); }
});
})();
</script>
<script src=\"/assets/js/529d45d105c9.js\" defer></script>
"
}
File diff suppressed because one or more lines are too long
+3 -442
View File
@@ -1708,7 +1708,7 @@ fn page_open() -> String {
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
margin-left: 2.5rem;
margin-left: 0;
align-self: flex-start;
transition: background 0.15s, border-color 0.15s, transform 0.1s;
line-height: 1;
@@ -1920,106 +1920,7 @@ fn page_open() -> String {
fn page_close() -> String {
return "
<script>
(function() {
'use strict';
// Nav scroll effect
var nav = document.getElementById('nav');
if (nav) {
window.addEventListener('scroll', function() {
if (window.scrollY > 40) {
nav.classList.add('scrolled');
} else {
nav.classList.remove('scrolled');
}
}, { passive: true });
}
// Auto-open chat if ?open=chat in URL
if (typeof URLSearchParams !== 'undefined' && new URLSearchParams(window.location.search).get('open') === 'chat') {
// Wait for widget to initialize, then open
setTimeout(function() { if (typeof neuronDemoToggle === 'function') neuronDemoToggle(); }, 600);
}
// Scroll reveal via IntersectionObserver
var revealEls = document.querySelectorAll('.reveal');
if ('IntersectionObserver' in window) {
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
revealEls.forEach(function(el) { observer.observe(el); });
} else {
revealEls.forEach(function(el) { el.classList.add('visible'); });
}
// ── Founding counter - live polling ──────────────────────────────────────
var prevSold = null;
function updateFoundingUI(data) {
var remaining = data.remaining;
var sold = data.sold;
var total = data.total;
var pct = Math.round((sold / total) * 100);
var flash = prevSold !== null && sold > prevSold;
prevSold = sold;
// spots card (inside pricing card)
var spotLabel = document.querySelector('.founding-spots-label');
if (spotLabel) spotLabel.textContent = 'Only ' + remaining + ' left';
var spotFill = document.querySelector('.founding-spots-fill');
if (spotFill) spotFill.style.width = pct + '%';
var spotSub = document.querySelector('.founding-spots-sub');
if (spotSub) spotSub.textContent = sold + ' of ' + total + ' claimed';
// banner
var bannerCount = document.querySelector('.founding-banner-count');
if (bannerCount) {
bannerCount.textContent = remaining;
if (flash) {
bannerCount.style.color = '#0078D4';
setTimeout(function() { bannerCount.style.color = ''; }, 1200);
}
}
var bannerFill = document.querySelector('.founding-banner-fill');
if (bannerFill) bannerFill.style.width = pct + '%';
}
function pollFoundingCount() {
fetch('/api/founding-count')
.then(function(r) { return r.json(); })
.then(function(data) { updateFoundingUI(data); })
.catch(function() {});
}
pollFoundingCount();
setInterval(pollFoundingCount, 90000);
// Hide chat on checkout, account, legal pages — not relevant there
if (window.location.pathname.indexOf('/checkout') === 0 ||
window.location.pathname.indexOf('/account') === 0 ||
window.location.pathname.indexOf('/legal') === 0 ||
window.location.pathname.indexOf('/marketplace/success') === 0) {
var demoBtn = document.getElementById('neuron-demo-btn');
var demoPanel = document.getElementById('neuron-demo-panel');
if (demoBtn) demoBtn.style.display = 'none';
if (demoPanel) demoPanel.style.display = 'none';
}
// Checkout buttons - navigate to integrated payment page
var checkoutBtns = document.querySelectorAll('[data-checkout]');
checkoutBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
var plan = btn.getAttribute('data-checkout');
window.location.href = '/checkout?plan=' + plan;
});
});
})();
</script>
<script src=\"/assets/js/407e72cd7182.js\" defer></script>
<!-- ── Neuron Demo Chat Widget ──────────────────────────────────────────────── -->
<div id=\"neuron-demo-btn\">
@@ -2055,347 +1956,7 @@ fn page_close() -> String {
</div>
</div>
<script>
(function() {
if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); }
var TURNSTILE_SITE_KEY = '0x4AAAAAADHAZXyuRb3yD9mr';
var turnstileToken = '';
var turnstileWidgetId = null;
var turnstileVerified = false;
var isOpen = false;
var MAX = 10;
// Persistent session storage - survives page refreshes
function loadSession() {
try {
var s = localStorage.getItem('neuron_demo_session');
return s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
} catch(e) { return { messages: [], count: 0, context: '' }; }
}
function saveSession(session) {
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
}
function clearSession() {
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
}
function _mg(s) { return s._m || { nodes: [], edges: [] }; }
function _um(s, nn, ne) {
if (!nn || !nn.length) return;
var g = _mg(s), nm = {}, ek = function(e) { return e.from+'->'+e.to; }, em = {};
g.nodes.forEach(function(n) { nm[n.id] = n; });
(nn || []).forEach(function(n) {
if (nm[n.id]) { nm[n.id].w = Math.min(1.0, (nm[n.id].w || 0.5) + 0.08); }
else { nm[n.id] = n; }
});
g.nodes = Object.values(nm);
g.edges.forEach(function(e) { em[ek(e)] = e; });
(ne || []).forEach(function(e) {
var k = ek(e);
if (em[k]) { em[k].weight = Math.min(1.0, (em[k].weight || 0.5) + 0.05); }
else { em[k] = e; }
});
g.edges = Object.values(em);
s._m = g; saveSession(s);
}
function _ra(g, q) {
if (!g || !g.nodes || !g.nodes.length) return [];
var words = q.toLowerCase().split(/\s+/).filter(function(w) { return w.length > 3; });
var sc = {};
g.nodes.forEach(function(n) {
var t = (n.content || '').toLowerCase();
sc[n.id] = words.filter(function(w) { return t.indexOf(w) !== -1; }).length * 0.6 + (n.w || 0.5) * 0.4;
});
(g.edges || []).forEach(function(e) {
if (sc[e.from] > 0.1) sc[e.to] = (sc[e.to] || 0) + sc[e.from] * (e.weight || 0.5) * 0.4;
});
return g.nodes.filter(function(n) { return sc[n.id] > 0.2; })
.sort(function(a,b) { return sc[b.id]-sc[a.id]; }).slice(0,5)
.map(function(n) { return { id: n.id, content: n.content, score: sc[n.id] }; });
}
// ?reset=1 clears the session and reloads clean
if (window.location.search.indexOf('reset=1') !== -1) {
clearSession();
var clean = window.location.pathname;
window.history.replaceState({}, '', clean);
}
var session = loadSession();
// Ensure every user has a stable unique session ID.
if (!session.uid) {
session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
saveSession(session);
}
var msgCount = session.count || 0;
function updateCountdown() {
var el = document.getElementById('neuron-demo-countdown');
if (!el) return;
var remaining = MAX - msgCount;
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
// Always bold white. The number itself counts down naturally; the
// colour-shift was loud and made the chat feel rationed even when
// there were plenty of turns left.
el.style.color = '#ffffff';
el.style.fontWeight = '700';
}
window.neuronDemoReset = function() {
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
session = { messages: [], count: 0, context: '' };
msgCount = 0;
var msgs = document.getElementById('neuron-demo-messages');
if (msgs) msgs.innerHTML = '';
var input = document.getElementById('neuron-demo-text');
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
var btn = document.getElementById('neuron-demo-send');
if (btn) btn.disabled = false;
addMsg('ai', 'Hey. What is on your mind?', true);
};
window.neuronDemoToggle = function() {
isOpen = !isOpen;
var panel = document.getElementById('neuron-demo-panel');
if (panel) panel.style.display = isOpen ? 'flex' : 'none';
// Hide launch button on mobile when panel is open (panel covers it)
var btn = document.getElementById('neuron-demo-btn');
if (btn) btn.style.display = isOpen ? 'none' : '';
var msgs = document.getElementById('neuron-demo-messages');
if (isOpen && turnstileVerified && msgs && msgs.style.display !== 'none' && msgs.children.length === 0) {
// Restore previous conversation from localStorage so the visitor
// picks up where they left off. Only greet once - the `greeted` flag
// sticks in the session so reopening the panel doesn't replay the
// canned hello.
if (session.messages && session.messages.length > 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
var remaining = MAX - msgCount;
if (remaining <= 0) {
var input = document.getElementById('neuron-demo-text');
if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; }
}
} else if (!session.greeted) {
addMsg('ai', 'Hey. What is on your mind?', true);
session.greeted = true;
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
}
}
var input = document.getElementById('neuron-demo-text');
if (isOpen && input && !input.disabled) input.focus();
// Init Turnstile on first open
updateCountdown();
if (isOpen && !turnstileWidgetId && typeof turnstile !== 'undefined') {
var container = document.getElementById('neuron-demo-turnstile');
if (container) {
turnstileWidgetId = turnstile.render(container, {
sitekey: TURNSTILE_SITE_KEY,
size: 'compact',
callback: function(token) {
turnstileToken = token;
turnstileVerified = true;
// Destroy the widget completely
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
try { turnstile.remove(turnstileWidgetId); } catch(e) {}
turnstileWidgetId = null;
}
// Swap gate for chat
var gate = document.getElementById('neuron-demo-gate');
var msgs = document.getElementById('neuron-demo-messages');
var inputRow = document.getElementById('neuron-demo-input-row');
if (gate) gate.style.display = 'none';
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
// Show opening message
addMsg('ai', 'Hey. What is on your mind?', true);
updateCountdown();
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
'expired-callback': function() {
turnstileToken = '';
turnstileVerified = false;
}
});
}
}
};
function addMsg(role, text, skipSave) {
var msgs = document.getElementById('neuron-demo-messages');
if (!msgs) return null;
var el = document.createElement('div');
el.className = 'demo-msg demo-msg-' + role;
// Avatar - use DOM API to avoid quote escaping issues
var avatar = document.createElement('div');
avatar.className = 'demo-msg-avatar';
if (role === 'ai') {
var img = document.createElement('img');
img.src = '/assets/brand/neuron-brain.png';
img.alt = 'Neuron';
avatar.appendChild(img);
} else {
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '14'); svg.setAttribute('height', '14');
svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '2');
var p1 = document.createElementNS(svgNS, 'path');
p1.setAttribute('d', 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2');
var c1 = document.createElementNS(svgNS, 'circle');
c1.setAttribute('cx', '12'); c1.setAttribute('cy', '7'); c1.setAttribute('r', '4');
svg.appendChild(p1); svg.appendChild(c1);
avatar.appendChild(svg);
}
// Bubble
var bubble = document.createElement('div');
bubble.className = 'demo-msg-bubble';
if (role === 'ai' && typeof marked !== 'undefined') {
try { bubble.innerHTML = marked.parse(text); } catch(e) { bubble.textContent = text; }
} else {
bubble.textContent = text;
}
if (role === 'ai') {
var bodyWrap = document.createElement('div');
bodyWrap.className = 'demo-msg-ai-body';
bodyWrap.appendChild(bubble);
if (!skipSave) {
var shareBtn = document.createElement('button');
shareBtn.className = 'demo-share-pill';
shareBtn.title = 'Share this response';
shareBtn.textContent = 'Share ↗';
shareBtn.onclick = async function() {
var prevUser = '';
if (session.messages) {
for (var i = session.messages.length - 1; i >= 0; i--) {
if (session.messages[i].role === 'user') { prevUser = session.messages[i].text; break; }
}
}
shareBtn.style.opacity = '0.4';
try {
var r = await fetch('/api/share', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({question: prevUser, answer: text})
});
var d = await r.json();
if (d.id) window.open('/share/' + d.id, '_blank');
} catch(e) {}
shareBtn.style.opacity = '1';
};
bodyWrap.appendChild(shareBtn);
}
el.appendChild(avatar);
el.appendChild(bodyWrap);
} else {
el.appendChild(avatar);
el.appendChild(bubble);
}
msgs.appendChild(el);
msgs.scrollTop = msgs.scrollHeight;
// Persist to localStorage unless restoring
if (!skipSave && role !== 'thinking') {
session.messages = session.messages || [];
session.messages.push({ role: role, text: text });
// Keep last 40 messages to avoid storage bloat
if (session.messages.length > 40) session.messages = session.messages.slice(-40);
saveSession(session);
}
return el;
}
window.neuronDemoSend = async function() {
if (msgCount >= MAX) return;
var input = document.getElementById('neuron-demo-text');
var btn = document.getElementById('neuron-demo-send');
if (!input || btn.disabled) return;
var msg = input.value.trim();
if (!msg) return;
input.value = '';
btn.disabled = true;
addMsg('user', msg);
// 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 = '<span></span><span></span><span></span>';
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
var hist = (session.messages || []).slice(-20).filter(function(m){ return m.role !== 'thinking'; }).map(function(m){
return {role: m.role === 'ai' ? 'assistant' : 'user', content: m.text};
});
// Activate local engram nodes relevant to this message
var activated_nodes = _ra(session._m, msg);
// questions_remaining tells the soul how many turns the visitor has
// LEFT after this one. is_last_question is true when this turn is
// the visitor final turn under the rate limit, so the soul can
// close the conversation in voice instead of leaving them on a hard cap.
var questionsRemaining = (MAX - msgCount) - 1;
if (questionsRemaining < 0) questionsRemaining = 0;
var r = await fetch('/api/demo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: msg,
history: hist,
cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '',
uid: session.uid || '',
activated_nodes: activated_nodes,
engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0,
questions_remaining: questionsRemaining,
is_last_question: questionsRemaining === 0
})
});
var d = await r.json();
if (thinking) thinking.remove();
// Merge session nodes returned by soul into local engram
_um(session, d.sn, d.se);
var reply = d.response || d.reply || d.message || '';
var isError = !reply || reply === 'Stepped out for a moment. Try again.';
if (!isError) {
// Only count as an interaction on a real response
msgCount++;
session.count = msgCount;
saveSession(session);
updateCountdown();
if (msgCount >= MAX && input) {
input.disabled = true;
input.placeholder = 'Interaction limit reached';
}
}
addMsg('ai', reply || 'Stepped out for a moment. Try again.');
} catch(e) {
if (thinking) thinking.remove();
addMsg('ai', 'Stepped out for a moment. Try again.');
}
if (msgCount < MAX && btn) btn.disabled = false;
if (input) input.focus();
};
var inp = document.getElementById('neuron-demo-text');
if (inp) {
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); window.neuronDemoSend(); }
});
}
})();
</script>
<script src=\"/assets/js/fc247ef45b1d.js\" defer></script>
</body>
</html>
"