Files
neuron-web/src/account.el
T
will.anderson 21a7c07547 Add reasoning model recommendation to API Keys card
Callout above the provider list recommends o4-mini/o3, Claude Sonnet 4,
Gemini 2.5 Pro, or Grok-3 for best performance, notes that model choice
happens in the app, and points to Neuron Inference launching Q3 2026.
2026-05-11 12:54:28 -05:00

995 lines
42 KiB
EmacsLisp

// components/account.el - Account dashboard page.
// Client-rendered: El serves the HTML shell, JS handles auth + data.
from founding_badge import { founding_badge, founding_badge_css }
extern fn el_html_doc(lang: String, head: String, body: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_meta(attrs: String) -> String
extern fn el_title(text: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
extern fn el_script_inline(code: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_img(attrs: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
fn account_css() -> String {
let css: String = "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #FAFAF8;
--bg2: #F0F0EC;
--card: #FFFFFF;
--navy: #0052A0;
--navy-d: rgba(0,82,160,.06);
--navy-m: rgba(0,82,160,.12);
--navy-b: rgba(0,82,160,.20);
--navy-65: rgba(0,82,160,.65);
--navy-85: rgba(0,82,160,.85);
--t1: #0D0D14;
--t2: #3A3A4A;
--t3: #6B6B7E;
--t4: rgba(13,13,20,.35);
--border: rgba(0,0,0,.07);
--border2: rgba(0,0,0,.13);
--head: 'Playfair Display', Georgia, serif;
--body: 'IBM Plex Sans', system-ui, sans-serif;
}
html { scroll-behavior: smooth; }
html, body {
background: var(--bg);
color: var(--t1);
font-family: var(--body);
font-weight: 400;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
linear-gradient(rgba(0,0,0,.022) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,.022) 1px, transparent 1px);
background-size: 48px 48px;
}
#nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background: rgba(250,250,248,.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
.nav-inner {
max-width: 1280px;
margin: 0 auto;
padding: 0 2rem;
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-logo img { height: 2rem; width: auto; display: block; }
.nav-back {
font-family: var(--body);
font-size: 0.675rem;
font-weight: 400;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--t3);
text-decoration: none;
transition: color 200ms;
}
.nav-back:hover { color: var(--t1); }
.page-shell {
position: relative;
z-index: 1;
min-height: 100vh;
padding: 6rem 2rem 4rem;
}
.page-inner {
max-width: 680px;
margin: 0 auto;
}
.page-heading { margin-bottom: 2.5rem; }
.page-eyebrow {
font-family: var(--body);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--navy-65);
margin-bottom: 0.75rem;
}
.page-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 600;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--t1);
}
.account-section {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.card-dark {
background: var(--card);
border: 1px solid rgba(0,0,0,.07);
box-shadow: 0 2px 12px rgba(13,13,20,.04);
padding: 2rem;
}
.card-label {
font-family: var(--body);
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--navy-65);
margin-bottom: 1.25rem;
}
.plan-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
}
.plan-name {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--t1);
margin-bottom: 0.375rem;
}
.plan-status {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--navy);
background: rgba(0,82,160,.07);
border: 1px solid rgba(0,82,160,.18);
padding: 0.3rem 0.75rem;
}
.plan-status-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--navy);
flex-shrink: 0;
}
.plan-meta {
margin-top: 1.25rem;
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.plan-meta-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.plan-meta-label {
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--t3);
}
.plan-meta-value {
font-size: 0.875rem;
font-weight: 400;
color: var(--t1);
}
.timeline {
display: flex;
flex-direction: column;
gap: 0;
}
.timeline-item {
display: flex;
gap: 1.25rem;
padding-bottom: 1.5rem;
position: relative;
}
.timeline-item:last-child { padding-bottom: 0; }
.timeline-left {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 1.5rem;
}
.timeline-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--navy);
background: var(--bg);
flex-shrink: 0;
margin-top: 3px;
}
.timeline-dot.filled { background: var(--navy); }
.timeline-line {
flex: 1;
width: 1px;
background: rgba(0,82,160,.15);
margin-top: 4px;
}
.timeline-content {
flex: 1;
padding-bottom: 0.25rem;
}
.timeline-date {
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--navy-65);
margin-bottom: 0.375rem;
}
.timeline-text {
font-family: var(--body);
font-weight: 400;
font-size: 0.9rem;
color: var(--t1);
line-height: 1.6;
}
.timeline-sub {
font-family: var(--body);
font-weight: 300;
font-size: 0.8125rem;
color: var(--t2);
line-height: 1.6;
margin-top: 0.25rem;
}
.devices-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.device-icon {
width: 2.5rem;
height: 2.5rem;
border: 1px solid rgba(0,82,160,.18);
background: rgba(0,82,160,.04);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.devices-count {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.25rem;
font-weight: 600;
color: var(--t1);
}
.devices-sub { font-size: 0.8125rem; color: var(--t2); }
.devices-note {
font-family: var(--body);
font-weight: 300;
font-size: 0.8125rem;
color: var(--t3);
line-height: 1.65;
border-top: 1px solid var(--border);
padding-top: 1rem;
margin-top: 0.5rem;
}
.badge-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.badge-caption {
margin-top: 1.25rem;
font-size: 0.75rem;
font-weight: 400;
letter-spacing: 0.04em;
color: var(--t3);
text-align: center;
}
.signin-wrap {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 3rem 1rem;
}
.signin-icon {
width: 4rem;
height: 4rem;
border: 1px solid rgba(0,82,160,.20);
background: rgba(0,82,160,.04);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
}
.signin-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
color: var(--t1);
margin-bottom: 0.75rem;
}
.signin-sub {
font-family: var(--body);
font-weight: 300;
font-size: 0.9375rem;
color: var(--t2);
max-width: 24rem;
margin: 0 auto 2rem;
line-height: 1.65;
}
.signin-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
width: 100%;
box-sizing: border-box;
font-family: var(--body);
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--t1);
background: var(--card);
border: 1px solid var(--border2);
padding: 0.875rem 1.5rem;
cursor: pointer;
transition: border-color 200ms, background 200ms, box-shadow 200ms;
text-decoration: none;
}
.signin-btn:hover {
border-color: rgba(0,82,160,.35);
background: rgba(0,82,160,.03);
box-shadow: 0 2px 8px rgba(0,82,160,.08);
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-family: var(--body);
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.15em;
text-transform: uppercase;
text-decoration: none;
color: #fff;
background: var(--navy);
padding: 1rem 2rem;
border: none;
cursor: pointer;
transition: background 300ms, box-shadow 300ms;
box-shadow: 0 2px 16px rgba(0,82,160,.20);
}
.btn-primary:hover { background: #0078D4; }
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-family: var(--body);
font-size: 0.75rem;
font-weight: 400;
letter-spacing: 0.15em;
text-transform: uppercase;
text-decoration: none;
color: var(--t2);
background: transparent;
padding: 0.875rem 1.75rem;
border: 1px solid var(--border2);
cursor: pointer;
transition: border-color 300ms, color 300ms;
}
.btn-ghost:hover { border-color: rgba(0,82,160,.35); color: var(--t1); }
.action-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.action-row-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-chip { display: flex; align-items: center; gap: 0.625rem; }
.user-avatar {
width: 2rem;
height: 2rem;
border-radius: 50%;
background: rgba(0,82,160,.12);
border: 1px solid rgba(0,82,160,.20);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: var(--navy);
overflow: hidden;
flex-shrink: 0;
}
.user-avatar img { width: 100%; height: 100%; object-fit: cover; }
.user-email { font-size: 0.8125rem; color: var(--t2); }
.skeleton {
background: linear-gradient(90deg, rgba(0,0,0,.05) 25%, rgba(0,0,0,.08) 50%, rgba(0,0,0,.05) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.4s infinite;
border-radius: 3px;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.navy-line {
height: 1px;
background: linear-gradient(90deg, rgba(0,82,160,.30) 0%, rgba(0,82,160,.08) 100%);
}
.acct-section-header { margin-bottom: 1.25rem; }
.acct-input {
display: block;
width: 100%;
font-family: var(--body);
font-size: 0.875rem;
font-weight: 400;
color: var(--t1);
background: var(--bg);
border: 1px solid var(--border2);
padding: 0.75rem 1rem;
margin-bottom: 0.625rem;
outline: none;
transition: border-color 200ms;
}
.acct-input:focus { border-color: rgba(0,82,160,.45); }
.acct-input::placeholder { color: var(--t4); }
@media (max-width: 600px) {
.plan-row { flex-direction: column; }
.card-dark { padding: 1.5rem; }
.page-shell { padding: 5.5rem 1.25rem 3rem; }
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--navy); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: #0078D4; }
.acct-email { font-family: var(--body); font-size: .875rem; color: var(--t2); font-weight: 300; }
.acct-header-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; }
.status-badge-preorder { display: inline-flex; align-items: center; gap: .375rem; font-size: .65rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: #92600A; background: rgba(146,96,10,.08); border: 1px solid rgba(146,96,10,.2); padding: .3rem .75rem; }
.download-btn-disabled { display: inline-flex; align-items: center; gap: .75rem; font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; color: var(--t3); background: var(--bg2); padding: 1rem 2rem; border: 1px solid var(--border2); cursor: not-allowed; }
.plan-billing-note { font-family: var(--body); font-size: .8125rem; font-weight: 300; color: var(--t2); margin-top: 1rem; line-height: 1.6; }
.plan-billing-link { color: var(--navy-65); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; background: none; border: none; font-family: var(--body); font-size: .8125rem; font-weight: 300; padding: 0; }
.plan-billing-link:hover { color: var(--navy); }
.download-status { font-size: .65rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: var(--t3); margin-bottom: .75rem; }
.download-title { font-family: 'Playfair Display', Georgia, serif; font-size: 1.25rem; font-weight: 600; color: var(--t1); margin-bottom: .5rem; }
.download-body { font-family: var(--body); font-weight: 300; font-size: .875rem; color: var(--t2); line-height: 1.7; margin-bottom: 1.5rem; }
.roadmap-list { display: flex; flex-direction: column; gap: 0; }
.roadmap-phase { padding: 1.25rem 0; border-bottom: 1px solid var(--border); }
.roadmap-phase-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .875rem; }
.roadmap-phase-label { font-family: var(--body); font-size: .7rem; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--navy); }
.roadmap-phase-date { font-family: var(--body); font-size: .7rem; font-weight: 400; color: var(--t3); letter-spacing: .06em; }
.roadmap-items { list-style: none; display: flex; flex-direction: column; gap: .5rem; }
.roadmap-items li { font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t2); line-height: 1.6; padding-left: 1rem; position: relative; }
.roadmap-items li::before { content: \"-\"; position: absolute; left: 0; color: var(--navy-65); }
.signout-section { padding-top: 1rem; display: flex; justify-content: flex-end; }
.api-key-list { display: flex; flex-direction: column; gap: 1.5rem; }
.api-key-entry { border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; }
.api-key-entry:last-child { border-bottom: none; padding-bottom: 0; }
.api-key-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: .625rem; }
.api-key-name { font-size: .875rem; font-weight: 500; color: var(--t1); }
.api-key-masked { font-size: .75rem; font-family: monospace; color: var(--t3); }
.api-key-row { display: flex; align-items: center; gap: .75rem; }
.api-key-actions { display: flex; gap: .5rem; flex-shrink: 0; }
.api-key-help { margin-top: .625rem; }
.api-key-help summary { font-size: .75rem; font-weight: 500; letter-spacing: .03em; color: var(--navy-65); cursor: pointer; list-style: none; padding: .25rem 0; user-select: none; }
.api-key-help summary::-webkit-details-marker { display: none; }
.api-key-help summary::before { content: \"\\25B8 \"; font-size: .6rem; }
.api-key-help[open] summary::before { content: \"\\25BE \"; }
.api-key-help-body { padding: .75rem 0 .125rem; }
.api-key-help-body ol { padding-left: 1.25rem; display: flex; flex-direction: column; gap: .375rem; }
.api-key-help-body li { font-size: .8125rem; font-weight: 300; color: var(--t2); line-height: 1.55; }
.api-key-help-body a { color: var(--navy-65); text-decoration: underline; text-underline-offset: 2px; }
.api-key-help-body a:hover { color: var(--navy); }
.api-key-note { font-size: .75rem; font-weight: 300; color: var(--t3); margin-top: .625rem; line-height: 1.55; padding: .5rem .75rem; background: var(--bg2); border-left: 2px solid var(--navy-b); }
.api-key-note a { color: var(--navy-65); text-decoration: underline; text-underline-offset: 2px; }
.api-key-model-note { font-size: .8125rem; font-weight: 300; color: var(--t2); line-height: 1.65; padding: .75rem 1rem; background: var(--navy-d); border-left: 2px solid var(--navy); margin-bottom: 1.5rem; }
.api-key-model-note strong { font-weight: 600; color: var(--t1); }"
"<style>" + css + "</style>"
}
fn account_nav() -> String {
let logo_img: String = el_img("src=\"/assets/brand/neuron-wordmark-on-light.png\" srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" alt=\"Neuron\" height=\"28\"")
el_nav(
"id=\"nav\"",
el_div(
"class=\"nav-inner\"",
el_a("/", "class=\"nav-logo\" aria-label=\"Neuron home\"", logo_img) +
el_a("/", "class=\"nav-back\"", "&#8592; Home")
)
)
}
fn account_signin_svg_user() -> String {
"<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"rgba(0,82,160,.70)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/>" +
"<circle cx=\"12\" cy=\"7\" r=\"4\"/>" +
"</svg>"
}
fn account_signin_svg_google() -> String {
"<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" aria-hidden=\"true\">" +
"<path d=\"M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z\" fill=\"#4285F4\"/>" +
"<path d=\"M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z\" fill=\"#34A853\"/>" +
"<path d=\"M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z\" fill=\"#FBBC05\"/>" +
"<path d=\"M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z\" fill=\"#EA4335\"/>" +
"</svg>"
}
fn account_signin_svg_github() -> String {
"<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">" +
"<path d=\"M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z\"/>" +
"</svg>"
}
fn account_signin_svg_download() -> String {
"<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/>" +
"<polyline points=\"7 10 12 15 17 10\"/>" +
"<line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/>" +
"</svg>"
}
fn account_signin_svg_device() -> String {
"<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"rgba(0,82,160,.70)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\"/>" +
"<line x1=\"8\" y1=\"21\" x2=\"16\" y2=\"21\"/>" +
"<line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"21\"/>" +
"</svg>"
}
fn account_signin_section() -> String {
let or_divider: String = el_div(
"style=\"display:flex;align-items:center;gap:1rem;margin:.125rem 0\"",
el_div("style=\"flex:1;height:1px;background:var(--border2)\"", "") +
el_span("style=\"font-family:var(--body);font-size:.75rem;color:var(--t3);letter-spacing:.06em\"", "or") +
el_div("style=\"flex:1;height:1px;background:var(--border2)\"", "")
)
let email_input: String = "<input type=\"email\" id=\"acct-email-input\" placeholder=\"Email address\" autocomplete=\"email\" style=\"font-family:var(--body);font-size:.875rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid var(--border2);padding:.875rem 1rem;outline:none;transition:border-color .2s;width:100%;box-sizing:border-box\">"
let email_btn: String = "<button type=\"button\" id=\"acct-magic-btn\" onclick=\"sendMagicLink()\" style=\"font-family:var(--body);font-size:.75rem;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:#fff;background:var(--navy);border:none;padding:.875rem 1rem;cursor:pointer;transition:background .2s;width:100%;box-sizing:border-box\">Continue with email</button>"
let email_msg: String = el_p("id=\"acct-email-msg\" style=\"display:none;font-size:.8rem;text-align:center;margin-top:.25rem\"", "")
let options_wrap: String = el_div(
"style=\"width:100%;max-width:20rem;margin:0 auto;display:flex;flex-direction:column;gap:.75rem\"",
el_button("type=\"button\" class=\"signin-btn\" id=\"btn-google\" onclick=\"signInWith('google')\"", account_signin_svg_google() + " Continue with Google") +
el_button("type=\"button\" class=\"signin-btn\" id=\"btn-github\" onclick=\"signInWith('github')\"", account_signin_svg_github() + " Continue with GitHub") +
or_divider +
email_input +
email_btn +
email_msg
)
el_div(
"id=\"signin-section\"",
el_div(
"class=\"signin-wrap\"",
el_div("class=\"signin-icon\"", account_signin_svg_user()) +
el_h1("class=\"signin-title\"", "Sign in to your account") +
el_p("class=\"signin-sub\"", "Enter your email to receive a sign-in link, or continue with a social account.") +
options_wrap
)
)
}
fn account_plan_card() -> String {
el_div(
"class=\"card-dark\" id=\"plan-card\"",
el_p("class=\"card-label\"", "Your Plan") +
el_div(
"class=\"plan-row\"",
el_div(
"",
el_p("class=\"plan-name\" id=\"plan-name-el\"", "Loading...") +
el_div("id=\"plan-status-el\"", "") +
el_div("id=\"plan-billing-note-el\"", "")
)
) +
el_div("class=\"plan-meta\" id=\"plan-meta-el\"", "")
)
}
fn account_roadmap_phase1() -> String {
el_div(
"class=\"roadmap-phase\"",
el_div(
"class=\"roadmap-phase-header\"",
el_span("class=\"roadmap-phase-label\"", "Now shipping") +
el_span("class=\"roadmap-phase-date\"", "Q3 2026")
) +
el_ul(
"class=\"roadmap-items\"",
el_li("", "macOS and Windows clients") +
el_li("", "Persistent memory &mdash; local, encrypted") +
el_li("", "Bring your own API keys (OpenAI, Anthropic, Grok)") +
el_li("", "Neuron Inference &mdash; priced below the major APIs") +
el_li("", "Gmail, Slack, Google Calendar connectors") +
el_li("", "VS Code extension") +
el_li("", "Founding member badge in-app")
)
)
}
fn account_roadmap_phase2() -> String {
el_div(
"class=\"roadmap-phase\"",
el_div(
"class=\"roadmap-phase-header\"",
el_span("class=\"roadmap-phase-label\"", "Following launch") +
el_span("class=\"roadmap-phase-date\"", "Q4 2026")
) +
el_ul(
"class=\"roadmap-items\"",
el_li("", "Process and knowledge packets") +
el_li("", "Local inference via Ollama") +
el_li("", "Additional connectors &mdash; GitHub, Notion, Linear, more") +
el_li("", "Mobile companion app") +
el_li("", "Family accounts")
)
)
}
fn account_roadmap_phase3() -> String {
el_div(
"class=\"roadmap-phase\" style=\"border-bottom:none\"",
el_div(
"class=\"roadmap-phase-header\"",
el_span("class=\"roadmap-phase-label\"", "2027") +
el_span("class=\"roadmap-phase-date\"", "H1 2027")
) +
el_ul(
"class=\"roadmap-items\"",
el_li("", "Imprints &mdash; starting with C-suite (CEO, CTO, CFO, CMO, COO)") +
el_li("", "Enterprise accounts") +
el_li("", "Plugin marketplace (open to developers)") +
el_li("", "API access for power users")
)
)
}
fn account_roadmap_section() -> String {
el_div(
"id=\"roadmap-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\"",
el_p("class=\"card-label\"", "Product Roadmap") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:.875rem;color:var(--t2);line-height:1.7;margin-bottom:1.5rem\"",
"As a Founding Member, you have visibility into what we&#39;re building. This is the real roadmap &mdash; not marketing."
) +
el_div("class=\"roadmap-list\"", account_roadmap_phase1() + account_roadmap_phase2() + account_roadmap_phase3()) +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:.75rem;color:var(--t3);margin-top:1.5rem;line-height:1.7\"",
"This roadmap is directional, not a commitment. Founding Members shape what gets built and when through their feedback and votes. Your input has real weight."
)
)
)
}
fn account_family_section() -> String {
let child_attest: String = "<label style=\"display:flex;gap:.5rem;align-items:flex-start;cursor:pointer;margin:.75rem 0\">" +
"<input type=\"checkbox\" id=\"family-attest\" style=\"margin-top:.2rem;accent-color:var(--navy)\">" +
"<span style=\"font-size:.8rem;color:var(--t2)\">I confirm this person is a minor under my care and is using this account under my supervision.</span>" +
"</label>"
el_div(
"id=\"family-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\"",
el_div(
"class=\"acct-section-header\"",
el_p("class=\"card-label\"", "Family members") +
el_p("style=\"font-size:.8125rem;font-weight:300;color:var(--t2);line-height:1.65\"", "Add up to 5 children to your Founding Member plan. $10/month per child.")
) +
el_div("id=\"family-list\"", "") +
el_div(
"id=\"add-child-form\"",
el_input("email", "id=\"child-email\" placeholder=\"Child&#39;s email\" class=\"acct-input\"") +
el_input("number", "id=\"child-dob-year\" placeholder=\"Birth year (e.g. 2012)\" min=\"1990\" max=\"2007\" class=\"acct-input\"") +
child_attest +
el_button("class=\"btn-primary\" onclick=\"addFamilyMember()\" style=\"padding:.75rem 1.5rem\"", "Add family member") +
el_p("id=\"family-msg\" style=\"display:none;font-size:.8rem;margin-top:.5rem\"", "")
)
)
)
}
fn account_badge_section() -> String {
el_div(
"id=\"badge-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\" style=\"padding:2.5rem 2rem;text-align:center;border-top:3px solid var(--navy);background:linear-gradient(135deg,rgba(0,82,160,.03) 0%,rgba(0,82,160,.06) 100%)\"",
el_p("class=\"card-label\" style=\"margin-bottom:1.5rem;letter-spacing:.25em\"", "Your Founding Member Badge") +
el_div("id=\"badge-html-container\" style=\"display:flex;justify-content:center;margin-bottom:1.5rem\"", "") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:.8125rem;color:var(--t3);line-height:1.7;max-width:24rem;margin:0 auto\"",
"This badge will appear in the Neuron app at launch. Your number is permanent."
)
)
)
}
fn account_timeline_item1() -> String {
el_div(
"class=\"timeline-item\"",
el_div(
"class=\"timeline-left\"",
el_div("class=\"timeline-dot filled\"", "") +
el_div("class=\"timeline-line\"", "")
) +
el_div(
"class=\"timeline-content\"",
el_p("class=\"timeline-date\"", "Now") +
el_p("class=\"timeline-text\"", "Your preorder is confirmed.") +
el_p("class=\"timeline-sub\"", "A confirmation email has been sent to your address. Thank you for being here early.")
)
)
}
fn account_timeline_item2() -> String {
el_div(
"class=\"timeline-item\"",
el_div(
"class=\"timeline-left\"",
el_div("class=\"timeline-dot filled\" style=\"background:rgba(0,82,160,.4);border-color:rgba(0,82,160,.4)\"", "") +
el_div("class=\"timeline-line\"", "")
) +
el_div(
"class=\"timeline-content\"",
el_p("class=\"timeline-date\" style=\"color:var(--navy);font-weight:600\"", "Within 30 days") +
el_p("class=\"timeline-text\"", "Neuron for Mac &amp; Windows ships.") +
el_p("class=\"timeline-sub\"", "You&#39;ll receive an email with your download link and license key. macOS and Windows simultaneously.")
)
)
}
fn account_timeline_item3() -> String {
el_div(
"class=\"timeline-item\"",
el_div(
"class=\"timeline-left\"",
el_div("class=\"timeline-dot\"", "")
) +
el_div(
"class=\"timeline-content\"",
el_p("class=\"timeline-date\"", "Q3 2026") +
el_p("class=\"timeline-text\"", "Neuron Inference launches.") +
el_p("class=\"timeline-sub\"", "Our own inference layer, priced below the major APIs. No setup required - it activates automatically for your plan.")
)
)
}
fn account_timeline_card() -> String {
el_div(
"class=\"card-dark\" id=\"timeline-card\"",
el_p("class=\"card-label\"", "What happens next") +
el_div(
"class=\"timeline\"",
account_timeline_item1() +
account_timeline_item2() +
account_timeline_item3()
)
)
}
fn account_download_card() -> String {
el_div(
"class=\"card-dark\" style=\"border-top:3px solid var(--navy)\"",
el_p("class=\"card-label\"", "Download") +
el_p("class=\"download-status\" style=\"color:var(--navy)\"", "Shipping within 30 days") +
el_p("class=\"download-title\"", "Neuron for Mac &amp; Windows") +
el_p("class=\"download-body\"", "macOS and Windows simultaneously. You&#39;ll receive a download link and license key by email the moment it ships. Nothing to do right now.") +
el_button("type=\"button\" class=\"download-btn-disabled\" disabled aria-disabled=\"true\"",
account_signin_svg_download() + " Download arriving soon"
)
)
}
fn account_devices_card() -> String {
el_div(
"class=\"card-dark\"",
el_p("class=\"card-label\"", "Devices") +
el_div(
"class=\"devices-row\"",
el_div("class=\"device-icon\"", account_signin_svg_device()) +
el_div(
"",
el_p("class=\"devices-count\"", "2 devices included with your plan") +
el_p("class=\"devices-sub\"", "Currently: Setup at launch")
)
) +
el_p("class=\"devices-note\"", "Device activation happens when you install. Your license key will be in your launch email.")
)
}
fn api_key_provider_row(provider_id: String, provider_name: String, placeholder: String, instructions: String) -> String {
el_div(
"class=\"api-key-entry\"",
el_div(
"class=\"api-key-header\"",
el_span("class=\"api-key-name\"", provider_name) +
el_span("class=\"api-key-masked\" id=\"apikey-masked-" + provider_id + "\"", "Not configured")
) +
el_div(
"class=\"api-key-row\"",
"<input type=\"password\" id=\"apikey-input-" + provider_id + "\" class=\"acct-input\" placeholder=\"" + placeholder + "\" autocomplete=\"off\" style=\"margin-bottom:0;flex:1\">" +
el_div(
"class=\"api-key-actions\"",
el_button("type=\"button\" class=\"btn-primary\" style=\"padding:.5rem 1rem;font-size:.75rem\" onclick=\"saveApiKey('" + provider_id + "')\"", "Save") +
el_button("type=\"button\" class=\"btn-ghost\" style=\"padding:.5rem 1rem;font-size:.75rem;display:none\" id=\"apikey-del-" + provider_id + "\" onclick=\"deleteApiKey('" + provider_id + "')\"", "Remove")
)
) +
instructions
)
}
fn account_api_keys_section() -> String {
let openai_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get an OpenAI key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\" rel=\"noopener\">platform.openai.com/api-keys &#8599;</a></li>" +
"<li>Click <strong>Create new secret key</strong></li>" +
"<li>Give it a name (e.g. &#8220;Neuron&#8221;), then click <strong>Create secret key</strong></li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">You need billing set up before making API calls. Add a payment method at <a href=\"https://platform.openai.com/settings/organization/billing\" target=\"_blank\" rel=\"noopener\">platform.openai.com &#8599;</a></p>" +
"</div></details>"
let anthropic_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get an Anthropic key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://console.anthropic.com/settings/keys\" target=\"_blank\" rel=\"noopener\">console.anthropic.com/settings/keys &#8599;</a></li>" +
"<li>Click <strong>Create Key</strong></li>" +
"<li>Give it a name (e.g. &#8220;Neuron&#8221;), then click <strong>Create Key</strong></li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">You need to add credits before making API calls. Go to <a href=\"https://console.anthropic.com/settings/plans\" target=\"_blank\" rel=\"noopener\">console.anthropic.com &#8594; Plans &amp; Billing &#8599;</a> to add credits.</p>" +
"</div></details>"
let gemini_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get a Gemini key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://aistudio.google.com/apikey\" target=\"_blank\" rel=\"noopener\">aistudio.google.com/apikey &#8599;</a></li>" +
"<li>Sign in with your Google account if prompted</li>" +
"<li>Click <strong>Create API key</strong></li>" +
"<li>Select an existing Google Cloud project or create a new one</li>" +
"<li>Copy the key</li>" +
"</ol>" +
"<p class=\"api-key-note\">The free tier (Gemini 1.5 Flash) has generous rate limits. Paid usage is billed through your Google Cloud account.</p>" +
"</div></details>"
let grok_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get a Grok key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://console.x.ai/\" target=\"_blank\" rel=\"noopener\">console.x.ai &#8599;</a></li>" +
"<li>Sign in with your X (Twitter) account</li>" +
"<li>In the left sidebar, click <strong>API Keys</strong></li>" +
"<li>Click <strong>Create API Key</strong>, give it a name</li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">xAI offers free monthly credits to new accounts. Usage beyond the free tier is billed per token.</p>" +
"</div></details>"
let providers: String = el_div(
"class=\"api-key-list\"",
api_key_provider_row("openai", "OpenAI", "sk-...", openai_help) +
api_key_provider_row("anthropic", "Anthropic", "sk-ant-...", anthropic_help) +
api_key_provider_row("gemini", "Gemini", "AIza...", gemini_help) +
api_key_provider_row("grok", "Grok", "xai-...", grok_help)
)
el_div(
"id=\"api-keys-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\"",
el_div(
"class=\"acct-section-header\"",
el_p("class=\"card-label\"", "API Keys") +
el_p("style=\"font-size:.8125rem;font-weight:300;color:var(--t2);line-height:1.65\"",
"Add your own AI provider keys. Neuron uses them directly &#8212; your keys, your models, your data."
)
) +
el_div(
"class=\"api-key-model-note\"",
"<strong>For best performance, use a reasoning model.</strong> o4-mini or o3 (OpenAI) &#183; Claude Sonnet 4 (Anthropic) &#183; Gemini 2.5 Pro (Google) &#183; Grok-3 (xAI). You choose the model in the app &#8212; any model works, reasoning models are where Neuron shines. <a href=\"#\" style=\"color:var(--navy-65);text-decoration:underline;text-underline-offset:2px\">Neuron Inference</a> &#8212; our own model layer, priced below the major APIs &#8212; launches Q3 2026 and becomes the default."
) +
providers +
el_p("id=\"api-keys-msg\" style=\"display:none;font-size:.8rem;margin-top:.75rem\"", "")
)
)
}
fn account_dashboard_section() -> String {
let header_row: String = el_div(
"class=\"acct-header-row\"",
el_div(
"",
el_p("class=\"page-eyebrow\" style=\"margin-bottom:.375rem\"", "Account") +
el_h1("class=\"page-title\" style=\"margin-bottom:.5rem\"", "Your Account") +
el_p("class=\"acct-email\" id=\"acct-header-email\"", "")
) +
el_div(
"style=\"padding-top:.25rem\"",
el_button("type=\"button\" class=\"btn-ghost\" id=\"signout-btn-top\" onclick=\"signOut()\"", "Sign out")
)
)
let signout_row: String = el_div(
"class=\"signout-section\"",
el_button("type=\"button\" class=\"btn-ghost\" id=\"signout-btn\" onclick=\"signOut()\"", "Sign out")
)
el_div(
"id=\"dashboard-section\" style=\"display:none\"",
header_row +
el_div(
"class=\"account-section\"",
account_plan_card() +
account_api_keys_section() +
account_roadmap_section() +
account_family_section() +
account_badge_section() +
account_timeline_card() +
account_download_card() +
account_devices_card() +
signout_row
)
)
}
fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
let head: String =
el_meta_charset("UTF-8") +
el_meta("name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"") +
el_title("My Account - Neuron") +
el_meta("name=\"description\" content=\"Manage your Neuron account, view your plan, and access your founding member details.\"") +
"<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/assets/favicon-16.png\">" +
"<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/favicon-32.png\">" +
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">" +
"<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>" +
el_link_stylesheet("https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400;1,500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap") +
account_css()
let body: String =
account_nav() +
el_div(
"class=\"page-shell\"",
el_div(
"class=\"page-inner\"",
account_signin_section() +
account_dashboard_section()
)
) +
el_script_src("https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js", false) +
el_script_inline("window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";") +
el_script_src("/js/account-auth.js", true) +
el_script_src("/js/account-dashboard.js", true)
el_html_doc("lang=\"en\"", head, body)
}