diff --git a/src/js/chat-widget.el b/src/js/chat-widget.el index a1c1c46..6390859 100644 --- a/src/js/chat-widget.el +++ b/src/js/chat-widget.el @@ -142,14 +142,27 @@ fn main() -> Void { } } + function _todayUTC() { return Math.floor(Date.now() / 86400000); } function loadSession() { try { var s = localStorage.getItem('neuron_demo_session'); - return s ? JSON.parse(s) : { messages: [], count: 0, context: '' }; + var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' }; + // Reset count (and conversation) on new UTC day — keeps client in sync with server + var today = _todayUTC(); + if (parsed.day !== today) { + parsed.count = 0; + parsed.messages = []; + parsed.greeted = false; + parsed.day = today; + } + return parsed; } catch(e) { return { messages: [], count: 0, context: '' }; } } function saveSession(session) { - try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {} + try { + if (!session.day) session.day = _todayUTC(); + localStorage.setItem('neuron_demo_session', JSON.stringify(session)); + } catch(e) {} } function clearSession() { try { localStorage.removeItem('neuron_demo_session'); } catch(e) {} @@ -525,34 +538,33 @@ fn main() -> Void { // Server-side rate limit — show a live countdown to reset if (d.rate_limited && d.reset_at) { var _showRateTimer = function() { - var now = Math.floor(Date.now() / 1000); + var now = Math.floor(Date.now() / 1000); var secsLeft = Math.max(0, d.reset_at - now); - var hh = Math.floor(secsLeft / 3600); - var mm = Math.floor((secsLeft % 3600) / 60); - var ss = secsLeft % 60; - var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; - var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss)); - return \"You've had 10 conversations today. Come back in \" + ts + \".\"; + var hh = Math.floor(secsLeft / 3600); + var mm = Math.floor((secsLeft % 3600) / 60); + var ss = secsLeft % 60; + var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; + return \"You've reached today's limit. Resets in \" + hh + ':' + pad(mm) + ':' + pad(ss) + '.'; }; addMsg('ai', _showRateTimer()); - // Update the last ai message with a live ticker + // Update the bubble text with a live ticker var _timerInterval = setInterval(function() { - var thMsgsInner = document.getElementById('neuron-demo-msgs'); - if (!thMsgsInner) { clearInterval(_timerInterval); return; } - var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai'); - var lastAi = aiMsgs[aiMsgs.length - 1]; - if (lastAi) { lastAi.textContent = _showRateTimer(); } + var msgsEl = document.getElementById('neuron-demo-messages'); + if (!msgsEl) { clearInterval(_timerInterval); return; } + var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai'); + var lastAi = aiMsgs[aiMsgs.length - 1]; + var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null; + if (lastBubble) { lastBubble.textContent = _showRateTimer(); } if (Math.floor(Date.now() / 1000) >= d.reset_at) { clearInterval(_timerInterval); - if (lastAi) { lastAi.textContent = \"You're all set — conversations reset. Say hello!\"; } - if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } - if (btn) { btn.disabled = false; } + if (lastBubble) { lastBubble.textContent = \"You're all set — conversations reset. Say hello!\"; } + if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } + if (btn) { btn.disabled = false; } + msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown(); } }, 1000); if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; } if (btn) { btn.disabled = true; } - if (btn) { btn.disabled = false; } - if (input) { input.focus(); } return; } diff --git a/src/main.el b/src/main.el index ef491a3..59374c0 100644 --- a/src/main.el +++ b/src/main.el @@ -873,6 +873,32 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String return "{\"rows\":" + ac_resp + "}" } + // ── Admin: reset all demo rate limits ──────────────────────────────────── + // POST { "admin_token": "" } + // Deletes all rows from demo_rate_limits — resets every user's daily quota. + if str_eq(path, "/api/admin/reset-rate-limits") { + if !str_eq(method, "POST") { + return "{\"__status__\":405,\"error\":\"POST required\"}" + } + let rrl_token_in: String = json_get(body, "admin_token") + let rrl_token_exp: String = env("NEURON_ADMIN_TOKEN") + if str_eq(rrl_token_exp, "") { + return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}" + } + if !str_eq(rrl_token_in, rrl_token_exp) { + return "{\"__status__\":401,\"error\":\"unauthorized\"}" + } + let rrl_sb_url: String = state_get("__supabase_project_url__") + let rrl_sb_key: String = state_get("__supabase_service_key__") + if str_eq(rrl_sb_url, "") || str_eq(rrl_sb_key, "") { + return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}" + } + // DELETE /rest/v1/demo_rate_limits?uid=not.is.null (all rows) + let rrl_url: String = rrl_sb_url + "/rest/v1/demo_rate_limits?uid=not.is.null" + let _rrl_resp: String = http_delete_auth(rrl_url, rrl_sb_key, rrl_sb_key) + return "{\"ok\":true,\"message\":\"rate limits cleared\"}" + } + // ── My plan: server-side waitlist read with JWT verification ───────────── // POST { "access_token": "" }. We verify the JWT via Supabase // /auth/v1/user, extract the email, then read the waitlist row with the diff --git a/src/pricing.el b/src/pricing.el index f1b0599..4d937eb 100644 --- a/src/pricing.el +++ b/src/pricing.el @@ -51,9 +51,9 @@ fn pricing_pro_features() -> String { } fn pricing_founding_features() -> String { - el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) + + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - pay-per-use at the founding member rate, below the major APIs")) + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) + - el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) + + el_li("", el_span("class=\"dash\"", "-") + el_span("", "No subscription — software updates are free forever")) + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) + @@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String { el_span("class=\"pricing-price\"", "$199") + el_span("class=\"pricing-cadence\"", "lifetime") ) + - el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") + + el_p("class=\"pricing-tagline\"", "Pay once for the platform — free software updates, forever. Inference is pay-per-use at your founding member rate.") + spots_html + el_ul("class=\"pricing-features\"", pricing_founding_features()) + el_div("style=\"flex:1\"", "") +