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.html

bot.html (24139B)


      1 <!doctype html>
      2 <html lang="en">
      3 <head>
      4     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
      5     <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
      6     <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
      7     <meta charset="utf-8" />
      8     <meta name="viewport" content="width=device-width, initial-scale=1" />
      9     <title>Pytml Help Bot</title>
     10     <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'></text></svg>">
     11     <style>
     12         *,
     13         *::before,
     14         *::after {
     15             box-sizing: border-box;
     16             margin: 0;
     17             padding: 0;
     18         }
     19 
     20         :root {
     21             --bg: #0d0d0d;
     22             --surface: #1a1a1a;
     23             --surface-elevated: #252525;
     24             --surface-input: #2a2a2a;
     25             --text-primary: #e8eaed;
     26             --text-secondary: #9aa0a6;
     27             --text-muted: #5f6368;
     28             --border: #303030;
     29             --border-focus: #4a4a4a;
     30             --accent: #8ab4f8;
     31             --accent-hover: #6a9ef0;
     32             --accent-glow: rgba(138, 180, 248, 0.15);
     33             --user-bubble: #2a2a2a;
     34             --bot-avatar: linear-gradient(135deg, #8ab4f8, #34a853);
     35             --radius: 28px;
     36             --radius-sm: 16px;
     37             --radius-xs: 12px;
     38             --transition: 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
     39             --shadow: 0 4px 24px rgba(0, 0, 0, 0.6);
     40             --shadow-input: 0 2px 12px rgba(0, 0, 0, 0.5);
     41         }
     42 
     43         html,
     44         body {
     45             height: 100%;
     46             background: var(--bg);
     47             color: var(--text-primary);
     48             font-family: "Google Sans", "Segoe UI", Roboto, system-ui, -apple-system, Arial, sans-serif;
     49             overflow: hidden;
     50         }
     51 
     52         /* ─── APP SHELL ─── */
     53         .app {
     54             display: flex;
     55             flex-direction: column;
     56             height: 100vh;
     57             max-height: 100dvh;
     58             background: var(--bg);
     59             position: relative;
     60             overflow: hidden;
     61         }
     62 
     63         /* ─── HEADER ─── */
     64         .header {
     65             display: flex;
     66             align-items: center;
     67             justify-content: space-between;
     68             padding: 12px 24px;
     69             flex-shrink: 0;
     70             background: var(--bg);
     71             z-index: 10;
     72             border-bottom: 1px solid transparent;
     73         }
     74         .header .logo {
     75             font-size: 20px;
     76             font-weight: 500;
     77             letter-spacing: -0.3px;
     78             background: linear-gradient(90deg, #8ab4f8, #aecbfa, #8ab4f8);
     79             -webkit-background-clip: text;
     80             background-clip: text;
     81             color: transparent;
     82             display: flex;
     83             align-items: center;
     84             gap: 8px;
     85         }
     86         .header .logo::before {
     87             content: "";
     88             font-size: 22px;
     89             background: none;
     90             -webkit-background-clip: unset;
     91             background-clip: unset;
     92             color: #8ab4f8;
     93         }
     94         .header-actions {
     95             display: flex;
     96             gap: 8px;
     97         }
     98         .header-btn {
     99             width: 36px;
    100             height: 36px;
    101             border-radius: 50%;
    102             border: none;
    103             background: transparent;
    104             color: var(--text-secondary);
    105             font-size: 18px;
    106             cursor: pointer;
    107             display: flex;
    108             align-items: center;
    109             justify-content: center;
    110             transition: background var(--transition);
    111         }
    112         .header-btn:hover {
    113             background: var(--surface);
    114         }
    115 
    116         /* ─── MAIN CONTENT ─── */
    117         .main {
    118             flex: 1;
    119             display: flex;
    120             flex-direction: column;
    121             align-items: center;
    122             justify-content: center;
    123             padding: 0 20px 20px 20px;
    124             min-height: 0;
    125             transition: justify-content 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    126             position: relative;
    127         }
    128         .main.has-messages {
    129             justify-content: flex-start;
    130         }
    131 
    132         /* ─── GREETING ─── */
    133         .greeting {
    134             display: flex;
    135             flex-direction: column;
    136             align-items: center;
    137             text-align: center;
    138             width: 100%;
    139             max-width: 640px;
    140             padding: 0 8px;
    141             transition: opacity 0.4s ease, transform 0.4s ease;
    142             flex-shrink: 0;
    143         }
    144         .greeting.hidden {
    145             opacity: 0;
    146             pointer-events: none;
    147             transform: translateY(12px);
    148             position: absolute;
    149         }
    150         .greeting h1 {
    151             font-size: clamp(36px, 7vw, 52px);
    152             font-weight: 500;
    153             margin: 0 0 4px 0;
    154             background: linear-gradient(90deg, #8ab4f8, #aecbfa, #8ab4f8);
    155             -webkit-background-clip: text;
    156             background-clip: text;
    157             color: transparent;
    158             letter-spacing: -0.5px;
    159         }
    160         .greeting p {
    161             font-size: clamp(20px, 3.5vw, 30px);
    162             color: var(--text-secondary);
    163             font-weight: 400;
    164             margin: 0 0 0 0;
    165             letter-spacing: -0.2px;
    166         }
    167 
    168         /* ─── MESSAGES ─── */
    169         .messages-wrapper {
    170             flex: 1;
    171             width: 100%;
    172             max-width: 760px;
    173             overflow-y: auto;
    174             display: none;
    175             flex-direction: column;
    176             padding: 12px 0 8px 0;
    177             scroll-behavior: smooth;
    178             min-height: 0;
    179         }
    180         .messages-wrapper.visible {
    181             display: flex;
    182         }
    183 
    184         .messages-wrapper::-webkit-scrollbar {
    185             width: 4px;
    186         }
    187         .messages-wrapper::-webkit-scrollbar-track {
    188             background: transparent;
    189         }
    190         .messages-wrapper::-webkit-scrollbar-thumb {
    191             background: var(--border-focus);
    192             border-radius: 8px;
    193         }
    194 
    195         .messages {
    196             display: flex;
    197             flex-direction: column;
    198             gap: 6px;
    199             padding: 4px 8px 4px 4px;
    200             width: 100%;
    201         }
    202 
    203         /* ─── MESSAGE ROWS ─── */
    204         .msg-row {
    205             display: flex;
    206             gap: 14px;
    207             align-items: flex-start;
    208             padding: 10px 4px;
    209             animation: msgIn 0.3s ease forwards;
    210             opacity: 0;
    211             transform: translateY(8px);
    212         }
    213         @keyframes msgIn {
    214             to {
    215                 opacity: 1;
    216                 transform: translateY(0);
    217             }
    218         }
    219         .msg-row.user {
    220             flex-direction: row-reverse;
    221         }
    222 
    223         .msg-avatar {
    224             width: 32px;
    225             height: 32px;
    226             border-radius: 50%;
    227             flex-shrink: 0;
    228             background: var(--bot-avatar);
    229             display: flex;
    230             align-items: center;
    231             justify-content: center;
    232             font-size: 15px;
    233             color: #fff;
    234             font-weight: 500;
    235         }
    236         .msg-row.user .msg-avatar {
    237             background: linear-gradient(135deg, #8ab4f8, #6a9ef0);
    238             font-size: 14px;
    239         }
    240 
    241         .bubble {
    242             font-size: 15px;
    243             line-height: 1.7;
    244             white-space: pre-wrap;
    245             word-break: break-word;
    246             max-width: 85%;
    247             padding: 6px 0;
    248             color: var(--text-primary);
    249         }
    250         .msg-row.user .bubble {
    251             background: var(--user-bubble);
    252             padding: 10px 18px;
    253             border-radius: 20px 20px 4px 20px;
    254             max-width: 75%;
    255             border: 1px solid var(--border);
    256         }
    257         .msg-row.bot .bubble {
    258             padding: 6px 0;
    259             color: var(--text-primary);
    260         }
    261 
    262         /* ---- Markdown styling for bot messages (ADDED) ---- */
    263         .msg-row.bot .bubble h1,
    264         .msg-row.bot .bubble h2,
    265         .msg-row.bot .bubble h3 {
    266             margin: 0.5em 0 0.25em;
    267             font-weight: 600;
    268         }
    269         .msg-row.bot .bubble h1 { font-size: 1.5em; }
    270         .msg-row.bot .bubble h2 { font-size: 1.3em; }
    271         .msg-row.bot .bubble h3 { font-size: 1.1em; }
    272 
    273         .msg-row.bot .bubble ul,
    274         .msg-row.bot .bubble ol {
    275             padding-left: 1.5em;
    276             margin: 0.3em 0;
    277         }
    278         .msg-row.bot .bubble li {
    279             margin-bottom: 0.2em;
    280         }
    281 
    282         .msg-row.bot .bubble blockquote {
    283             border-left: 3px solid var(--accent);
    284             padding-left: 1em;
    285             margin: 0.5em 0;
    286             color: var(--text-secondary);
    287         }
    288 
    289         .msg-row.bot .bubble a {
    290             color: var(--accent);
    291             text-decoration: underline;
    292         }
    293         .msg-row.bot .bubble a:hover {
    294             color: var(--accent-hover);
    295         }
    296 
    297         .msg-row.bot .bubble pre {
    298             background: var(--surface-elevated);
    299             border-radius: var(--radius-xs);
    300             padding: 0;
    301             margin: 0.5em 0;
    302             overflow-x: auto;
    303             font-size: 13px;
    304             line-height: 1.6;
    305             border: 1px solid var(--border);
    306         }
    307         .msg-row.bot .bubble pre code {
    308             display: block;
    309             padding: 12px 16px;
    310             font-family: 'JetBrains Mono', monospace;
    311             font-size: 13px;
    312             background: transparent;
    313             color: #d4d4d4;
    314             white-space: pre;
    315             word-break: normal;
    316         }
    317         .msg-row.bot .bubble code:not(pre code) {
    318             background: var(--surface-elevated);
    319             padding: 2px 8px;
    320             border-radius: 6px;
    321             font-size: 13px;
    322             color: #d4d4d4;
    323             font-family: 'JetBrains Mono', monospace;
    324         }
    325 
    326         /* ─── TYPING DOTS ─── */
    327         .typing-dots {
    328             display: inline-flex;
    329             gap: 5px;
    330             padding: 8px 0;
    331         }
    332         .typing-dots span {
    333             width: 8px;
    334             height: 8px;
    335             border-radius: 50%;
    336             background: var(--text-secondary);
    337             animation: dotBounce 1.3s infinite ease-in-out;
    338         }
    339         .typing-dots span:nth-child(2) {
    340             animation-delay: 0.15s;
    341         }
    342         .typing-dots span:nth-child(3) {
    343             animation-delay: 0.3s;
    344         }
    345         @keyframes dotBounce {
    346             0%,
    347             60%,
    348             100% {
    349                 transform: translateY(0);
    350                 opacity: 0.35;
    351             }
    352             30% {
    353                 transform: translateY(-6px);
    354                 opacity: 1;
    355             }
    356         }
    357 
    358         /* ─── INPUT AREA ─── */
    359         .input-area {
    360             width: 100%;
    361             max-width: 760px;
    362             flex-shrink: 0;
    363             padding: 12px 0 4px 0;
    364             transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    365             display: flex;
    366             flex-direction: column;
    367             align-items: center;
    368             gap: 6px;
    369         }
    370         .main:not(.has-messages) .input-area {
    371             padding-top: 20px;
    372         }
    373         .main.has-messages .input-area {
    374             padding-top: 8px;
    375         }
    376 
    377         .composer {
    378             width: 100%;
    379             display: flex;
    380             align-items: center;
    381             gap: 8px;
    382             background: var(--surface-input);
    383             border: 1px solid var(--border);
    384             border-radius: var(--radius);
    385             padding: 4px 4px 4px 20px;
    386             transition: all var(--transition);
    387             box-shadow: var(--shadow-input);
    388         }
    389         .composer:focus-within {
    390             border-color: var(--accent);
    391             box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-input);
    392             background: var(--surface-elevated);
    393         }
    394 
    395         .composer input {
    396             flex: 1;
    397             border: none;
    398             outline: none;
    399             background: transparent;
    400             font-size: 15px;
    401             color: var(--text-primary);
    402             padding: 12px 0;
    403             font-family: inherit;
    404             min-width: 0;
    405         }
    406         .composer input::placeholder {
    407             color: var(--text-muted);
    408             font-weight: 400;
    409         }
    410 
    411         .composer .icon-btn {
    412             width: 40px;
    413             height: 40px;
    414             border-radius: 50%;
    415             border: none;
    416             background: transparent;
    417             color: var(--text-secondary);
    418             display: flex;
    419             align-items: center;
    420             justify-content: center;
    421             cursor: pointer;
    422             flex-shrink: 0;
    423             font-size: 18px;
    424             transition: all var(--transition);
    425         }
    426         .composer .icon-btn:hover {
    427             background: rgba(255, 255, 255, 0.06);
    428         }
    429 
    430         .composer .send-btn {
    431             background: var(--accent);
    432             color: #0d0d0d;
    433             width: 40px;
    434             height: 40px;
    435             border-radius: 50%;
    436             border: none;
    437             display: flex;
    438             align-items: center;
    439             justify-content: center;
    440             cursor: pointer;
    441             flex-shrink: 0;
    442             font-size: 18px;
    443             transition: all var(--transition);
    444         }
    445         .composer .send-btn:hover {
    446             background: var(--accent-hover);
    447             transform: scale(1.04);
    448         }
    449         .composer .send-btn:disabled {
    450             background: var(--border-focus);
    451             color: var(--text-muted);
    452             cursor: not-allowed;
    453             transform: none;
    454         }
    455 
    456         /* ─── DISCLAIMER ─── */
    457         .disclaimer {
    458             font-size: 11.5px;
    459             color: var(--text-muted);
    460             text-align: center;
    461             padding: 10px 0 2px 0;
    462             letter-spacing: 0.2px;
    463             user-select: none;
    464         }
    465         .disclaimer .grad {
    466             background: linear-gradient(90deg, #8ab4f8, #aecbfa);
    467             -webkit-background-clip: text;
    468             background-clip: text;
    469             color: transparent;
    470             font-weight: 500;
    471         }
    472 
    473         /* ─── RESPONSIVE ─── */
    474         @media (max-width: 600px) {
    475             .header {
    476                 padding: 10px 16px;
    477             }
    478             .header .logo {
    479                 font-size: 18px;
    480             }
    481             .main {
    482                 padding: 0 12px 16px 12px;
    483             }
    484             .greeting h1 {
    485                 font-size: 30px;
    486             }
    487             .greeting p {
    488                 font-size: 20px;
    489                 margin-bottom: 0;
    490             }
    491             .composer {
    492                 padding: 2px 2px 2px 14px;
    493                 border-radius: 24px;
    494             }
    495             .composer input {
    496                 font-size: 14px;
    497                 padding: 10px 0;
    498             }
    499             .composer .send-btn,
    500             .composer .icon-btn {
    501                 width: 36px;
    502                 height: 36px;
    503                 font-size: 16px;
    504             }
    505             .msg-row {
    506                 padding: 6px 0;
    507             }
    508             .bubble {
    509                 font-size: 14px;
    510             }
    511             .msg-row.user .bubble {
    512                 max-width: 85%;
    513                 padding: 8px 14px;
    514             }
    515         }
    516 
    517         @media (max-width: 420px) {
    518             .greeting h1 {
    519                 font-size: 26px;
    520             }
    521             .greeting p {
    522                 font-size: 17px;
    523             }
    524         }
    525 
    526         /* ─── SCROLLBAR GLOBAL ─── */
    527         ::-webkit-scrollbar {
    528             width: 4px;
    529         }
    530         ::-webkit-scrollbar-track {
    531             background: transparent;
    532         }
    533         ::-webkit-scrollbar-thumb {
    534             background: var(--border-focus);
    535             border-radius: 8px;
    536         }
    537     </style>
    538 </head>
    539 <body>
    540 
    541     <div class="app">
    542 
    543         <!-- ─── HEADER ─── -->
    544         <header class="header">
    545             <span class="logo">Pytml</span>
    546             <div class="header-actions">
    547                 <button class="header-btn" id="clearBtn" title="Clear chat">✕</button>
    548             </div>
    549         </header>
    550 
    551         <!-- ─── MAIN ─── -->
    552         <main class="main" id="main">
    553 
    554             <!-- Greeting -->
    555             <div class="greeting" id="greeting">
    556                 <h1>Hello</h1>
    557                 <p>How can I help you today?</p>
    558             </div>
    559 
    560             <!-- Messages -->
    561             <div class="messages-wrapper" id="messagesWrapper">
    562                 <div class="messages" id="messages"></div>
    563             </div>
    564 
    565             <!-- Input -->
    566             <div class="input-area" id="inputArea">
    567                 <form class="composer" id="form">
    568                     <input
    569                     id="input"
    570                     type="text"
    571                     placeholder="Ask Pytml"
    572                     autocomplete="off"
    573                     autofocus
    574                     />
    575                     <button type="button" class="icon-btn" id="attachBtn" title="Attach">📎</button>
    576                     <button type="submit" class="send-btn" id="sendBtn" title="Send">➤</button>
    577                 </form>
    578                 <div class="disclaimer">
    579                     <span class="made-with">Made with <span class="grad">Gemini</span></span>
    580                     · Please recheck responses
    581                 </div>
    582             </div>
    583 
    584         </main>
    585     </div>
    586 
    587     <script>
    588         (function() {
    589             'use strict';
    590 
    591             // ─── DOM refs ───
    592             const main = document.getElementById('main');
    593             const greeting = document.getElementById('greeting');
    594             const messagesWrapper = document.getElementById('messagesWrapper');
    595             const messages = document.getElementById('messages');
    596             const form = document.getElementById('form');
    597             const input = document.getElementById('input');
    598             const sendBtn = document.getElementById('sendBtn');
    599             const clearBtn = document.getElementById('clearBtn');
    600 
    601             let isProcessing = false;
    602 
    603             // ─── Helpers ───
    604             function scrollToBottom() {
    605                 requestAnimationFrame(() => {
    606                     messagesWrapper.scrollTop = messagesWrapper.scrollHeight;
    607                 });
    608             }
    609 
    610             function setHasMessages(has) {
    611                 main.classList.toggle('has-messages', has);
    612                 greeting.classList.toggle('hidden', has);
    613                 messagesWrapper.classList.toggle('visible', has);
    614             }
    615 
    616             // ─── Add message ───
    617             function addMessage(text, role) {
    618                 const row = document.createElement('div');
    619                 row.className = `msg-row ${role}`;
    620 
    621                 const avatar = document.createElement('div');
    622                 avatar.className = 'msg-avatar';
    623                 avatar.textContent = role === 'user' ? 'U' : '';
    624 
    625                 const bubble = document.createElement('div');
    626                 bubble.className = 'bubble';
    627 
    628                 if (role === 'bot') {
    629                     bubble.innerHTML = formatBotText(text);
    630                     // highlight code blocks inside this bubble
    631                     const codes = bubble.querySelectorAll('pre code');
    632                     codes.forEach((block) => {
    633                         hljs.highlightElement(block);
    634                     });
    635                 } else {
    636                     bubble.textContent = text;
    637                 }
    638 
    639                 row.appendChild(avatar);
    640                 row.appendChild(bubble);
    641                 messages.appendChild(row);
    642 
    643                 setHasMessages(true);
    644                 scrollToBottom();
    645             }
    646 
    647             function formatBotText(text) {
    648                 const html = marked.parse(text);
    649                 return html;
    650             }
    651 
    652             // ─── Typing indicator ───
    653             function showTyping() {
    654                 const row = document.createElement('div');
    655                 row.className = 'msg-row bot';
    656                 row.id = 'typingRow';
    657 
    658                 const avatar = document.createElement('div');
    659                 avatar.className = 'msg-avatar';
    660                 avatar.textContent = '';
    661 
    662                 const bubble = document.createElement('div');
    663                 bubble.className = 'bubble';
    664                 bubble.innerHTML = `<div class="typing-dots"><span></span><span></span><span></span></div>`;
    665 
    666                 row.appendChild(avatar);
    667                 row.appendChild(bubble);
    668                 messages.appendChild(row);
    669 
    670                 setHasMessages(true);
    671                 scrollToBottom();
    672                 return row;
    673             }
    674 
    675             function hideTyping() {
    676                 const row = document.getElementById('typingRow');
    677                 if (row) row.remove();
    678             }
    679 
    680             // ─── Send message to backend ───
    681             async function sendToBackend(message) {
    682                 const response = await fetch('https://pytml.vercel.app/api/chat', {
    683                     method: 'POST',
    684                     headers: {
    685                         'Content-Type': 'application/json',
    686                     },
    687                     body: JSON.stringify({ message }),
    688                 });
    689 
    690                 if (!response.ok) {
    691                     const errorText = await response.text();
    692                     throw new Error(`Server error (${response.status}): ${errorText}`);
    693                 }
    694 
    695                 const data = await response.json();
    696                 if (data.reply === undefined) {
    697                     throw new Error('Unexpected response format: missing "reply" field');
    698                 }
    699                 return data.reply;
    700             }
    701 
    702             // ─── Handle send ───
    703             async function handleSend() {
    704                 const text = input.value.trim();
    705                 if (!text || isProcessing) return;
    706 
    707                 isProcessing = true;
    708                 sendBtn.disabled = true;
    709 
    710                 addMessage(text, 'user');
    711                 input.value = '';
    712                 input.focus();
    713 
    714                 const typingRow = showTyping();
    715 
    716                 try {
    717                     const reply = await sendToBackend(text);
    718                     hideTyping();
    719                     addMessage(reply, 'bot');
    720                 } catch (err) {
    721                     hideTyping();
    722                     console.error('Chat error:', err);
    723                     addMessage(`Error: ${err.message || 'Something went wrong'}`, 'bot');
    724                 } finally {
    725                     isProcessing = false;
    726                     sendBtn.disabled = false;
    727                     scrollToBottom();
    728                 }
    729             }
    730 
    731             // ─── Clear chat ───
    732             function clearChat() {
    733                 messages.innerHTML = '';
    734                 setHasMessages(false);
    735                 input.value = '';
    736                 input.focus();
    737                 const typingRow = document.getElementById('typingRow');
    738                 if (typingRow) typingRow.remove();
    739                 isProcessing = false;
    740                 sendBtn.disabled = false;
    741             }
    742 
    743             // ─── Event listeners ───
    744             form.addEventListener('submit', (e) => {
    745                 e.preventDefault();
    746                 handleSend();
    747             });
    748 
    749             clearBtn.addEventListener('click', clearChat);
    750 
    751             document.addEventListener('keydown', (e) => {
    752                 if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
    753                     e.preventDefault();
    754                     input.focus();
    755                 }
    756                 if (e.key === 'Escape') {
    757                     input.blur();
    758                 }
    759             });
    760 
    761             document.getElementById('attachBtn').addEventListener('click', () => {
    762                 input.value = 'Can you help me with a file?';
    763                 input.focus();
    764             });
    765 
    766             // ─── Init ───
    767             setHasMessages(false);
    768             input.focus();
    769         })();
    770     </script>
    771 
    772 </body>
    773 </html>