Add AI chat backend via OpenRouter with knowledge-base fallback

POST /api/public/chat proxies to OpenRouter (model from
OPENROUTER_MODEL, default deepseek/deepseek-v4-flash:nitro) with a
system prompt built from live site config, sponsor queue, and strict
guardrails (no income claims, seed-phrase warnings, Telegram fallback).
Rate-limited per IP. Without OPENROUTER_API_KEY set, or on any API
error, the widget transparently falls back to the built-in knowledge
base, so the chat always works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-12 08:47:02 -05:00
parent 708068a9a7
commit ca5605e056
3 changed files with 72 additions and 2 deletions
+16 -2
View File
@@ -70,8 +70,22 @@
document.body.appendChild(fab);document.body.appendChild(panel);
const msgs=panel.querySelector('.chat-msgs'),chipsEl=panel.querySelector('.chat-chips'),form=panel.querySelector('.chat-input'),input=form.querySelector('input');
function addMsg(html,who){const d=document.createElement('div');d.className=`chat-msg ${who}`;if(who==='user'){d.textContent=html}else{d.innerHTML=html}msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight}
function ask(q){addMsg(q,'user');setTimeout(()=>{addMsg(answer(q),'bot');},250)}
function addMsg(html,who){const d=document.createElement('div');d.className=`chat-msg ${who}`;if(who==='user'){d.textContent=html}else{d.innerHTML=html}msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;return d}
function linkify(text){let h=esc(text);h=h.replace(/(https?:\/\/[^\s&<]+[^\s&<.,)])/g,u=>{const site=u.startsWith('https://rmcircle.saasy.top');const path=site?u.replace('https://rmcircle.saasy.top',''):u;return `<a href="${site?(path||'/'):u}"${site?'':' target="_blank" rel="noopener noreferrer"'}>${u}</a>`});return h.replace(/\n/g,'<br>')}
const history=[];let aiDown=false;
function stripHtml(h){const d=document.createElement('div');d.innerHTML=h;return d.textContent||''}
async function ask(q){
addMsg(q,'user');history.push({role:'user',content:q});
if(aiDown){const a=answer(q);addMsg(a,'bot');history.push({role:'assistant',content:stripHtml(a)});return}
const typing=addMsg('<span class="chat-typing">…</span>','bot');
try{
const r=await fetch('/api/public/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:history.slice(-8)})});
const d=await r.json();
typing.remove();
if(d.reply){addMsg(linkify(d.reply),'bot');history.push({role:'assistant',content:d.reply})}
else{if(d.fallback)aiDown=true;const a=answer(q);addMsg(a,'bot');history.push({role:'assistant',content:stripHtml(a)})}
}catch(e){typing.remove();aiDown=true;const a=answer(q);addMsg(a,'bot');history.push({role:'assistant',content:stripHtml(a)})}
}
let started=false;
function open(){
+2
View File
@@ -44,3 +44,5 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh
.chat-foot{padding:8px 14px 12px;font-size:12px;color:var(--muted);text-align:center}
.chat-foot a{color:var(--teal)}
.chat-hidden{display:none}
.chat-typing{letter-spacing:3px;animation:chatpulse 1s infinite}
@keyframes chatpulse{50%{opacity:.35}}