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>