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:
+14
-17
@@ -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
@@ -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."
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 · Never billed again</p>';
|
||||
} else if (plan === 'professional') {
|
||||
billingNote = '<p class=\"plan-billing-note\">Billed monthly · <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 →</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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
"
|
||||
|
||||
Reference in New Issue
Block a user