bot.js (4292B)
1 (function(){ 2 const form = document.getElementById('form'); 3 const input = document.getElementById('input'); 4 const messages = document.getElementById('messages'); 5 6 function appendMessage(text, who = 'bot'){ 7 const el = document.createElement('div'); 8 el.className = `msg ${who}`; 9 10 // if message looks like code (``` fenced or starts with 4-space), render a code block 11 if (/^```[\s\S]*```$/.test(text.trim()) || /^\s{4}/m.test(text)){ 12 // strip fenced code markers if present 13 const fenceMatch = text.match(/```(?:[a-zA-Z0-9-_]+)?\n([\s\S]*?)\n```/); 14 const code = fenceMatch ? fenceMatch[1] : text; 15 const pre = document.createElement('pre'); 16 pre.className = 'code'; 17 pre.textContent = code; 18 19 const wrapper = document.createElement('div'); 20 wrapper.appendChild(pre); 21 22 const copyBtn = document.createElement('button'); 23 copyBtn.className = 'copy'; 24 copyBtn.textContent = 'Copy'; 25 copyBtn.addEventListener('click', ()=>{ 26 navigator.clipboard.writeText(code).then(()=>{ 27 copyBtn.textContent = 'Copied'; 28 setTimeout(()=>copyBtn.textContent='Copy',1200); 29 }); 30 }); 31 32 wrapper.appendChild(copyBtn); 33 el.appendChild(wrapper); 34 } else { 35 el.textContent = text; 36 } 37 38 messages.appendChild(el); 39 messages.scrollTop = messages.scrollHeight; 40 } 41 42 function showTyping(){ 43 const el = document.createElement('div'); 44 el.className = 'msg bot typing'; 45 el.id = 'typing-indicator'; 46 el.innerHTML = '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'; 47 messages.appendChild(el); 48 messages.scrollTop = messages.scrollHeight; 49 } 50 51 function hideTyping(){ 52 const t = document.getElementById('typing-indicator'); 53 if(t) t.remove(); 54 } 55 56 form.addEventListener('submit', async (e)=>{ 57 e.preventDefault(); 58 const text = input.value.trim(); 59 if(!text) return; 60 appendMessage(text, 'user'); 61 input.value=''; 62 63 showTyping(); 64 65 try{ 66 const res = await fetch('https://pytml.vercel.app/api/chat', { 67 method:'POST', 68 headers:{ 'Content-Type':'application/json' }, 69 body: JSON.stringify({ message: text }) 70 }); 71 72 hideTyping(); 73 74 if(res.status === 404){ 75 appendMessage('Chat endpoint not found on this host (expected /api/chat). Ensure your serverless function is deployed and GIMINI_API is set in Vercel.', 'bot'); 76 return; 77 } 78 if(res.status === 429){ 79 const j = await res.json().catch(()=>({ error: 'Rate limited' })); 80 appendMessage(j.error || 'Rate limit exceeded: 5 questions per 24 hours per IP.', 'bot'); 81 return; 82 } 83 84 // Try parse JSON, but fall back to text if it's HTML or plain text 85 let data; 86 try { 87 data = await res.json(); 88 } catch (jsonErr) { 89 // not JSON (likely HTML error page). Show the text so debugging is easier. 90 const textBody = await res.text(); 91 appendMessage('Unexpected non-JSON response from /api/chat — see response below:', 'bot'); 92 appendMessage(textBody, 'bot'); 93 return; 94 } 95 96 if (data.error) appendMessage(data.error, 'bot'); 97 else appendMessage(data.reply || JSON.stringify(data), 'bot'); 98 99 }catch(err){ 100 hideTyping(); 101 console.error(err); 102 appendMessage('Network or server error. Check the browser console for details.', 'bot'); 103 } 104 }); 105 106 // keyboard shortcut: Enter to send, Shift+Enter for newline 107 input.addEventListener('keydown',(e)=>{ 108 if(e.key === 'Enter' && !e.shiftKey){ 109 e.preventDefault(); 110 form.requestSubmit(); 111 } 112 }); 113 114 // friendly local hint (not authoritative) 115 const LOCAL_KEY = 'pytml_bot_local_count'; 116 function incrLocal(){ 117 try{ 118 const v = JSON.parse(localStorage.getItem(LOCAL_KEY) || '[]'); 119 const now = Date.now(); 120 v.push(now); 121 const cutoff = now - 24*60*60*1000; 122 const filtered = v.filter(t=>t>cutoff); 123 localStorage.setItem(LOCAL_KEY, JSON.stringify(filtered)); 124 return filtered.length; 125 }catch(e){ return null } 126 } 127 form.addEventListener('submit', ()=>{ 128 const c = incrLocal(); 129 if(c) console.log('Local friendly count for this browser (not authoritative):', c); 130 }); 131 132 })();