Pytml

Pytml is a 15KB lightweight PyScript alternative — run Python in HTML with one script tag.

git clone https://github.com/notamitgamer/Pytml.git
· rebuilt hourlyOpen on GitHub
root / bot.js

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 })();