Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Infinity GRM</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: Arial, sans-serif; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| overflow-x: hidden; | |
| background: #fff; | |
| } | |
| /* ========================= | |
| HEADER | |
| ========================= */ | |
| header { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 80px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 40px; | |
| background: #fff; | |
| z-index: 1000; | |
| transition: background-color 0.3s ease; | |
| } | |
| header.dark { | |
| background: #000; | |
| } | |
| .logo img { | |
| height: 50px; | |
| width: auto; | |
| display: block; | |
| } | |
| .nav { | |
| position: absolute; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 40px; | |
| } | |
| .nav a { | |
| color: #000; | |
| text-decoration: none; | |
| font-weight: 600; | |
| font-size: 15px; | |
| transition: color 0.3s ease; | |
| } | |
| header.dark .nav a { | |
| color: #fff; | |
| } | |
| /* ========================= | |
| ACCOUNT | |
| ========================= */ | |
| .account { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| background: #000; | |
| display: grid; | |
| place-items: center; | |
| text-decoration: none; | |
| } | |
| .account svg { | |
| width: 23px; | |
| height: 23px; | |
| display: block; | |
| fill: #fff; | |
| } | |
| header.dark .account { | |
| background: #fff; | |
| } | |
| header.dark .account svg { | |
| fill: #000; | |
| } | |
| /* ========================= | |
| MAIN | |
| ========================= */ | |
| main { | |
| min-height: 100vh; | |
| padding-top: 80px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| /* ========================= | |
| HOME | |
| ========================= */ | |
| .home { | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| h1 { | |
| font-size: 4rem; | |
| margin-bottom: 30px; | |
| } | |
| /* ========================= | |
| INPUT BAR | |
| ========================= */ | |
| .input { | |
| width: min(680px, 90%); | |
| min-height: 68px; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| background: #fff; | |
| border: 1px solid #d1d5db; | |
| border-radius: 18px; | |
| box-shadow: | |
| 0 5px 25px rgba(0, 0, 0, 0.05); | |
| transition: | |
| border-color 0.2s ease, | |
| box-shadow 0.2s ease; | |
| } | |
| .input:focus-within { | |
| border-color: #aaa; | |
| box-shadow: | |
| 0 5px 25px rgba(0, 0, 0, 0.08); | |
| } | |
| /* ========================= | |
| TEXT INPUT | |
| ========================= */ | |
| .edit { | |
| width: 100%; | |
| min-height: 66px; | |
| max-height: 180px; | |
| padding: | |
| 18px | |
| 105px | |
| 18px | |
| 18px; | |
| outline: none; | |
| border: none; | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| font-size: 16px; | |
| line-height: 1.4; | |
| color: #111; | |
| cursor: text; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| /* | |
| Makes the contenteditable behave | |
| consistently after being cleared. | |
| */ | |
| caret-color: #000; | |
| } | |
| .edit:focus { | |
| outline: none; | |
| } | |
| /* ========================= | |
| ASK ME ANYTHING PLACEHOLDER | |
| ========================= */ | |
| /* | |
| Because this is contenteditable rather | |
| than a normal input, we use ::before. | |
| When the element is completely empty, | |
| "Ask me anything..." appears. | |
| Your JavaScript must clear the element | |
| with innerHTML = "" after sending. | |
| */ | |
| .edit:empty::before { | |
| content: attr(data-placeholder); | |
| color: #aaa; | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| /* | |
| Keep the placeholder visible when the | |
| contenteditable receives focus. | |
| */ | |
| .edit:focus:empty::before { | |
| color: #aaa; | |
| } | |
| /* ========================= | |
| BUTTON RESET | |
| ========================= */ | |
| button { | |
| border: 0; | |
| outline: 0; | |
| background: transparent; | |
| cursor: pointer; | |
| padding: 0; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| } | |
| /* ========================= | |
| MICROPHONE | |
| ========================= */ | |
| .voice { | |
| position: absolute; | |
| right: 52px; | |
| bottom: 14px; | |
| width: 38px; | |
| height: 38px; | |
| min-width: 38px; | |
| min-height: 38px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #777; | |
| background: transparent; | |
| transition: | |
| background-color 0.2s ease, | |
| color 0.2s ease, | |
| transform 0.15s ease; | |
| } | |
| .voice:hover { | |
| background: #eee; | |
| color: #333; | |
| } | |
| .voice:active { | |
| transform: scale(0.94); | |
| } | |
| .voice.active { | |
| background: #eee; | |
| color: #000; | |
| } | |
| /* ========================= | |
| SEND / UP ARROW | |
| ========================= */ | |
| /* | |
| INACTIVE STATE | |
| This matches the gray circle from | |
| your reference image. | |
| It appears even when the input is empty. | |
| */ | |
| .send { | |
| position: absolute; | |
| right: 10px; | |
| bottom: 14px; | |
| width: 38px; | |
| height: 38px; | |
| min-width: 38px; | |
| min-height: 38px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: #b3b3b3; | |
| color: #fff; | |
| transition: | |
| background-color 0.2s ease, | |
| color 0.2s ease, | |
| transform 0.15s ease; | |
| } | |
| /* | |
| INACTIVE HOVER | |
| */ | |
| .send:hover { | |
| background: #a5a5a5; | |
| color: #fff; | |
| } | |
| /* | |
| CLICK | |
| */ | |
| .send:active { | |
| transform: scale(0.94); | |
| } | |
| /* | |
| ACTIVE STATE | |
| JavaScript adds .active when the | |
| user enters text. | |
| */ | |
| .send.active { | |
| background: #000; | |
| color: #fff; | |
| } | |
| /* | |
| ACTIVE HOVER | |
| */ | |
| .send.active:hover { | |
| background: #222; | |
| color: #fff; | |
| } | |
| /* ========================= | |
| SVG ICONS | |
| ========================= */ | |
| .voice svg, | |
| .send svg { | |
| display: block; | |
| width: 20px; | |
| height: 20px; | |
| min-width: 20px; | |
| min-height: 20px; | |
| max-width: 20px; | |
| max-height: 20px; | |
| flex-shrink: 0; | |
| overflow: visible; | |
| } | |
| .voice svg { | |
| width: 21px; | |
| height: 21px; | |
| min-width: 21px; | |
| min-height: 21px; | |
| max-width: 21px; | |
| max-height: 21px; | |
| } | |
| .voice svg path, | |
| .send svg path { | |
| fill: none; | |
| stroke: currentColor; | |
| vector-effect: non-scaling-stroke; | |
| } | |
| /* ========================= | |
| MISSION BUTTON | |
| ========================= */ | |
| .mission-btn { | |
| margin-top: 20px; | |
| padding: | |
| 15px | |
| 35px; | |
| border-radius: 50px; | |
| background: #000; | |
| color: #fff; | |
| text-decoration: none; | |
| font-weight: bold; | |
| transition: | |
| background-color 0.2s ease, | |
| transform 0.2s ease; | |
| } | |
| .mission-btn:hover { | |
| background: #222; | |
| transform: translateY(-1px); | |
| } | |
| /* ========================= | |
| CHAT | |
| ========================= */ | |
| .chat { | |
| display: none; | |
| position: fixed; | |
| top: 80px; | |
| right: 0; | |
| bottom: 0; | |
| left: 0; | |
| flex-direction: column; | |
| background: #fff; | |
| z-index: 500; | |
| /* | |
| Prevent the page itself from scrolling. | |
| */ | |
| overflow: hidden; | |
| } | |
| /* ========================= | |
| CHAT MESSAGES | |
| ========================= */ | |
| .messages { | |
| flex: 1; | |
| min-height: 0; | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| padding: | |
| 35px | |
| 20px | |
| 150px; | |
| /* | |
| Keep scrolling inside the messages | |
| area instead of moving the webpage. | |
| */ | |
| overscroll-behavior: contain; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .messages-inner { | |
| max-width: 850px; | |
| margin: 0 auto; | |
| } | |
| /* ========================= | |
| MESSAGE | |
| ========================= */ | |
| .message { | |
| display: flex; | |
| margin-bottom: 25px; | |
| } | |
| /* ========================= | |
| USER MESSAGE | |
| ========================= */ | |
| .user { | |
| justify-content: flex-end; | |
| } | |
| .user div { | |
| max-width: 70%; | |
| background: #f1f1f1; | |
| padding: | |
| 12px | |
| 17px; | |
| border-radius: | |
| 20px | |
| 20px | |
| 6px | |
| 20px; | |
| color: #111; | |
| line-height: 1.5; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| /* ========================= | |
| AI MESSAGE | |
| ========================= */ | |
| .ai div { | |
| max-width: 85%; | |
| color: #111; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| /* ========================= | |
| CHAT BOTTOM INPUT | |
| ========================= */ | |
| .bottom { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| display: flex; | |
| justify-content: center; | |
| padding: | |
| 15px | |
| 20px | |
| 22px; | |
| background: | |
| linear-gradient( | |
| rgba(255, 255, 255, 0), | |
| rgba(255, 255, 255, 0.95) 40%, | |
| #fff 70% | |
| ); | |
| z-index: 10; | |
| } | |
| /* | |
| Keep the chat input white. | |
| */ | |
| .bottom .input { | |
| width: min(680px, 100%); | |
| background: #fff; | |
| } | |
| /* | |
| The same gray inactive send button | |
| applies to the chat input. | |
| */ | |
| .bottom .send { | |
| background: #b3b3b3; | |
| color: #fff; | |
| } | |
| .bottom .send:hover { | |
| background: #a5a5a5; | |
| color: #fff; | |
| } | |
| .bottom .send.active { | |
| background: #000; | |
| color: #fff; | |
| } | |
| .bottom .send.active:hover { | |
| background: #222; | |
| color: #fff; | |
| } | |
| /* ========================= | |
| MISSION | |
| ========================= */ | |
| #mission { | |
| min-height: 100vh; | |
| background: #000; | |
| color: #fff; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| padding: | |
| 100px | |
| 10%; | |
| } | |
| #mission h2 { | |
| font-size: 4rem; | |
| margin-bottom: 30px; | |
| } | |
| #mission p { | |
| max-width: 900px; | |
| font-size: 1.2rem; | |
| line-height: 1.9; | |
| } | |
| /* ========================= | |
| MOBILE | |
| ========================= */ | |
| @media (max-width: 700px) { | |
| header { | |
| height: 70px; | |
| padding: | |
| 0 | |
| 15px; | |
| } | |
| .logo img { | |
| height: 42px; | |
| } | |
| .nav { | |
| gap: 15px; | |
| } | |
| .nav a { | |
| font-size: 12px; | |
| } | |
| .account { | |
| width: 42px; | |
| height: 42px; | |
| } | |
| main { | |
| padding-top: 70px; | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| } | |
| .input { | |
| width: 92%; | |
| min-height: 64px; | |
| } | |
| .edit { | |
| min-height: 62px; | |
| padding: | |
| 17px | |
| 105px | |
| 17px | |
| 16px; | |
| } | |
| .chat { | |
| top: 70px; | |
| } | |
| .messages { | |
| padding: | |
| 30px | |
| 15px | |
| 150px; | |
| } | |
| .user div { | |
| max-width: 85%; | |
| } | |
| .ai div { | |
| max-width: 92%; | |
| } | |
| #mission { | |
| padding: | |
| 80px | |
| 8%; | |
| } | |
| #mission h2 { | |
| font-size: 2.5rem; | |
| } | |
| #mission p { | |
| font-size: 1rem; | |
| line-height: 1.7; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ========================= | |
| HEADER | |
| ========================= --> | |
| <header id="header"> | |
| <a | |
| class="logo" | |
| href="index.html" | |
| > | |
| <img | |
| src="https://copilot.microsoft.com/th/id/BCO.cc808577-1db9-4523-90a6-be7b2abee261.png" | |
| alt="InfinityGRM" | |
| > | |
| </a> | |
| <nav class="nav"> | |
| <a href="index.html"> | |
| Home | |
| </a> | |
| <a href="labs.html"> | |
| Labs | |
| </a> | |
| </nav> | |
| <a | |
| href="signup.html" | |
| class="account" | |
| aria-label="Account" | |
| > | |
| <svg | |
| viewBox="0 0 24 24" | |
| aria-hidden="true" | |
| > | |
| <path d="M12 12c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm0 2c-3.34 0-10 1.68-10 5v3h20v-3c0-3.32-6.66-5-10-5z"/> | |
| </svg> | |
| </a> | |
| </header> | |
| <!-- ========================= | |
| MAIN | |
| ========================= --> | |
| <main> | |
| <!-- ========================= | |
| HOME | |
| ========================= --> | |
| <section | |
| class="home" | |
| id="home" | |
| > | |
| <h1> | |
| InfinityGRM | |
| </h1> | |
| <div class="input"> | |
| <!-- HOME INPUT --> | |
| <div | |
| id="input" | |
| class="edit" | |
| contenteditable="true" | |
| role="textbox" | |
| aria-label="Ask me anything" | |
| data-placeholder="Ask me anything..." | |
| ></div> | |
| <!-- HOME DICTATION --> | |
| <button | |
| id="voice" | |
| class="voice" | |
| type="button" | |
| aria-label="Dictate" | |
| title="Dictate" | |
| > | |
| <svg | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| aria-hidden="true" | |
| > | |
| <path | |
| d="M12 1.5a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0v-7a3 3 0 0 0-3-3z" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| /> | |
| <path | |
| d="M19.5 10.5v1.5a7.5 7.5 0 0 1-15 0v-1.5" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| /> | |
| <path | |
| d="M12 19.5v3" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| /> | |
| </svg> | |
| </button> | |
| <!-- HOME SEND --> | |
| <button | |
| id="send" | |
| class="send" | |
| type="button" | |
| aria-label="Send" | |
| title="Send" | |
| > | |
| <svg | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| aria-hidden="true" | |
| > | |
| <path | |
| d="M12 19V5m0 0-6 6m6-6 6 6" | |
| stroke-width="2" | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| /> | |
| </svg> | |
| </button> | |
| </div> | |
| <!-- MISSION BUTTON --> | |
| <a | |
| href="#mission" | |
| class="mission-btn" | |
| > | |
| Our Mission | |
| </a> | |
| </section> | |
| <!-- ========================= | |
| CHAT | |
| ========================= --> | |
| <section | |
| class="chat" | |
| id="chat" | |
| > | |
| <!-- MESSAGE AREA --> | |
| <div | |
| class="messages" | |
| id="messagesContainer" | |
| > | |
| <div | |
| class="messages-inner" | |
| id="messages" | |
| ></div> | |
| </div> | |
| <!-- ========================= | |
| CHAT INPUT | |
| ========================= --> | |
| <div class="bottom"> | |
| <div class="input"> | |
| <!-- CHAT INPUT --> | |
| <div | |
| id="chatInput" | |
| class="edit" | |
| contenteditable="true" | |
| role="textbox" | |
| aria-label="Ask me anything" | |
| data-placeholder="Ask me anything..." | |
| ></div> | |
| <!-- CHAT DICTATION --> | |
| <button | |
| id="chatVoice" | |
| class="voice" | |
| type="button" | |
| aria-label="Dictate" | |
| title="Dictate" | |
| > | |
| <svg | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| aria-hidden="true" | |
| > | |
| <path | |
| d="M12 1.5a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0v-7a3 3 0 0 0-3-3z" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| /> | |
| <path | |
| d="M19.5 10.5v1.5a7.5 7.5 0 0 1-15 0v-1.5" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| /> | |
| <path | |
| d="M12 19.5v3" | |
| stroke-width="1.5" | |
| stroke-linecap="round" | |
| /> | |
| </svg> | |
| </button> | |
| <!-- CHAT SEND --> | |
| <button | |
| id="chatSend" | |
| class="send" | |
| type="button" | |
| aria-label="Send" | |
| title="Send" | |
| > | |
| <svg | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| aria-hidden="true" | |
| > | |
| <path | |
| d="M12 19V5m0 0-6 6m6-6 6 6" | |
| stroke-width="2" | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- ========================= | |
| MISSION | |
| ========================= --> | |
| <section id="mission"> | |
| <h2> | |
| Our Mission | |
| </h2> | |
| <p> | |
| Our mission is to develop safe superintelligence that elevates and | |
| benefits all of humanity. We believe in building advanced AI systems, | |
| infrastructure, and breakthroughs that empower individuals and help | |
| shape a flourishing future. Every contribution allows us to continue | |
| pioneering new frontiers, supporting ambitious research, and ensuring | |
| that the rewards of technological progress make a lasting difference | |
| for communities around the world. | |
| </p> | |
| </section> | |
| <script> | |
| /* ========================= | |
| ELEMENTS | |
| ========================= */ | |
| const header = | |
| document.getElementById("header"); | |
| const mission = | |
| document.getElementById("mission"); | |
| const home = | |
| document.getElementById("home"); | |
| const chat = | |
| document.getElementById("chat"); | |
| const input = | |
| document.getElementById("input"); | |
| const send = | |
| document.getElementById("send"); | |
| const chatInput = | |
| document.getElementById("chatInput"); | |
| const chatSend = | |
| document.getElementById("chatSend"); | |
| const messages = | |
| document.getElementById("messages"); | |
| const messagesContainer = | |
| document.getElementById("messagesContainer"); | |
| const voice = | |
| document.getElementById("voice"); | |
| const chatVoice = | |
| document.getElementById("chatVoice"); | |
| /* ========================= | |
| CHAT STATE | |
| ========================= */ | |
| /* | |
| This tells the header whether the user | |
| is currently inside the chat. | |
| When true, the header will ALWAYS remain white. | |
| */ | |
| let chatActive = false; | |
| /* ========================= | |
| SESSION ID | |
| ========================= */ | |
| /* | |
| One session ID is created for the entire | |
| browser session. | |
| This allows multiple questions to belong | |
| to the same conversation. | |
| */ | |
| const sessionId = | |
| "session_" + | |
| Math.random() | |
| .toString(36) | |
| .slice(2) + | |
| "_" + | |
| Date.now(); | |
| /* ========================= | |
| HEADER | |
| ========================= */ | |
| function updateHeader() { | |
| /* | |
| CRITICAL FIX: | |
| If chat is open, never turn the header black. | |
| The user can scroll through 50+ messages and | |
| the header will stay white. | |
| */ | |
| if (chatActive) { | |
| header.classList.remove("dark"); | |
| return; | |
| } | |
| /* | |
| Normal homepage behavior. | |
| The header becomes black when the Mission | |
| section reaches the top of the viewport. | |
| */ | |
| const missionTop = | |
| mission.getBoundingClientRect().top; | |
| header.classList.toggle( | |
| "dark", | |
| missionTop <= 100 | |
| ); | |
| } | |
| /* | |
| Listen for normal page scrolling. | |
| This does NOT affect the header while chatActive | |
| is true. | |
| */ | |
| window.addEventListener( | |
| "scroll", | |
| updateHeader | |
| ); | |
| /* | |
| Initial header state. | |
| */ | |
| window.addEventListener( | |
| "load", | |
| updateHeader | |
| ); | |
| /* ========================= | |
| GET CLEAN TEXT | |
| ========================= */ | |
| function getInputText(element) { | |
| return element.innerText | |
| .replace(/\u200B/g, "") | |
| .replace(/\u00A0/g, " ") | |
| .trim(); | |
| } | |
| /* ========================= | |
| CLEAR CONTENTEDITABLE | |
| ========================= */ | |
| function clearInput(element) { | |
| /* | |
| innerHTML = "" is important. | |
| contenteditable browsers can insert <br> | |
| elements that would otherwise prevent | |
| :empty from working correctly. | |
| */ | |
| element.innerHTML = ""; | |
| element.textContent = ""; | |
| } | |
| /* ========================= | |
| BUTTON STATE | |
| ========================= */ | |
| function updateButton( | |
| inputElement, | |
| buttonElement | |
| ) { | |
| const text = | |
| getInputText(inputElement); | |
| buttonElement.classList.toggle( | |
| "active", | |
| text.length > 0 | |
| ); | |
| } | |
| /* ========================= | |
| ADD MESSAGE | |
| ========================= */ | |
| function addMessage( | |
| text, | |
| type | |
| ) { | |
| const message = | |
| document.createElement("div"); | |
| message.className = | |
| "message " + type; | |
| const content = | |
| document.createElement("div"); | |
| content.textContent = | |
| text; | |
| message.appendChild( | |
| content | |
| ); | |
| messages.appendChild( | |
| message | |
| ); | |
| /* | |
| Scroll ONLY the chat message area. | |
| This is important because the page itself | |
| should not move when new messages arrive. | |
| */ | |
| requestAnimationFrame(() => { | |
| messagesContainer.scrollTop = | |
| messagesContainer.scrollHeight; | |
| }); | |
| } | |
| /* ========================= | |
| SHOW CHAT | |
| ========================= */ | |
| function showChat() { | |
| /* | |
| Mark chat as active FIRST. | |
| This prevents the header scroll handler | |
| from turning it black. | |
| */ | |
| chatActive = true; | |
| /* | |
| Immediately force the header white. | |
| */ | |
| header.classList.remove( | |
| "dark" | |
| ); | |
| /* | |
| Hide homepage. | |
| */ | |
| home.style.display = | |
| "none"; | |
| /* | |
| Show chat. | |
| */ | |
| chat.style.display = | |
| "flex"; | |
| /* | |
| Prevent the main document from scrolling | |
| while the chat is active. | |
| */ | |
| document.body.style.overflow = | |
| "hidden"; | |
| /* | |
| Focus the chat input. | |
| */ | |
| requestAnimationFrame(() => { | |
| chatInput.focus(); | |
| }); | |
| } | |
| /* ========================= | |
| SEND MESSAGE | |
| ========================= */ | |
| async function sendMessage( | |
| inputElement, | |
| buttonElement | |
| ) { | |
| const text = | |
| getInputText(inputElement); | |
| /* | |
| Do nothing if empty. | |
| */ | |
| if (!text) { | |
| return; | |
| } | |
| /* | |
| Open chat. | |
| */ | |
| showChat(); | |
| /* | |
| Add user's message immediately. | |
| */ | |
| addMessage( | |
| text, | |
| "user" | |
| ); | |
| /* | |
| Clear the input. | |
| This makes the placeholder | |
| "Ask me anything..." | |
| appear again. | |
| */ | |
| clearInput( | |
| inputElement | |
| ); | |
| /* | |
| Reset send button. | |
| */ | |
| updateButton( | |
| inputElement, | |
| buttonElement | |
| ); | |
| try { | |
| const response = await fetch( | |
| "ai-bridge.php", | |
| { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify({ | |
| message: text, | |
| session_id: sessionId | |
| }) | |
| } | |
| ); | |
| /* | |
| Check HTTP status. | |
| */ | |
| if (!response.ok) { | |
| throw new Error( | |
| "Server returned " + | |
| response.status | |
| ); | |
| } | |
| /* | |
| Convert response to JSON. | |
| */ | |
| const data = | |
| await response.json(); | |
| /* | |
| Add AI response. | |
| */ | |
| addMessage( | |
| data.response || | |
| ( | |
| "Error: " + | |
| ( | |
| data.error || | |
| "No response" | |
| ) | |
| ), | |
| "ai" | |
| ); | |
| } catch (error) { | |
| console.error( | |
| "Chat error:", | |
| error | |
| ); | |
| addMessage( | |
| "Failed to connect to the server.", | |
| "ai" | |
| ); | |
| } | |
| /* | |
| ALWAYS reset the chat input. | |
| This guarantees the placeholder is | |
| visible after the AI responds. | |
| */ | |
| clearInput( | |
| chatInput | |
| ); | |
| updateButton( | |
| chatInput, | |
| chatSend | |
| ); | |
| /* | |
| Keep the chat input ready for | |
| another question. | |
| */ | |
| requestAnimationFrame(() => { | |
| chatInput.focus(); | |
| }); | |
| } | |
| /* ========================= | |
| HOME INPUT | |
| ========================= */ | |
| input.addEventListener( | |
| "input", | |
| () => { | |
| updateButton( | |
| input, | |
| send | |
| ); | |
| } | |
| ); | |
| /* ========================= | |
| CHAT INPUT | |
| ========================= */ | |
| chatInput.addEventListener( | |
| "input", | |
| () => { | |
| updateButton( | |
| chatInput, | |
| chatSend | |
| ); | |
| } | |
| ); | |
| /* ========================= | |
| SEND BUTTON | |
| ========================= */ | |
| send.addEventListener( | |
| "click", | |
| () => { | |
| sendMessage( | |
| input, | |
| send | |
| ); | |
| } | |
| ); | |
| chatSend.addEventListener( | |
| "click", | |
| () => { | |
| sendMessage( | |
| chatInput, | |
| chatSend | |
| ); | |
| } | |
| ); | |
| /* ========================= | |
| ENTER TO SEND | |
| ========================= */ | |
| input.addEventListener( | |
| "keydown", | |
| event => { | |
| /* | |
| Enter = send | |
| Shift + Enter = new line | |
| */ | |
| if ( | |
| event.key === "Enter" && | |
| !event.shiftKey | |
| ) { | |
| event.preventDefault(); | |
| sendMessage( | |
| input, | |
| send | |
| ); | |
| } | |
| } | |
| ); | |
| chatInput.addEventListener( | |
| "keydown", | |
| event => { | |
| /* | |
| Enter = send | |
| Shift + Enter = new line | |
| */ | |
| if ( | |
| event.key === "Enter" && | |
| !event.shiftKey | |
| ) { | |
| event.preventDefault(); | |
| sendMessage( | |
| chatInput, | |
| chatSend | |
| ); | |
| } | |
| } | |
| ); | |
| /* ========================= | |
| DICTATION | |
| ========================= */ | |
| function setupDictation( | |
| button, | |
| targetInput, | |
| targetSendButton | |
| ) { | |
| const SpeechRecognition = | |
| window.SpeechRecognition || | |
| window.webkitSpeechRecognition; | |
| /* | |
| Browser doesn't support speech recognition. | |
| */ | |
| if (!SpeechRecognition) { | |
| button.addEventListener( | |
| "click", | |
| () => { | |
| alert( | |
| "Voice dictation is not supported in this browser." | |
| ); | |
| } | |
| ); | |
| return; | |
| } | |
| const recognition = | |
| new SpeechRecognition(); | |
| recognition.continuous = | |
| false; | |
| recognition.interimResults = | |
| false; | |
| recognition.lang = | |
| "en-US"; | |
| /* ========================= | |
| START | |
| ========================= */ | |
| button.addEventListener( | |
| "click", | |
| () => { | |
| try { | |
| recognition.start(); | |
| button.classList.add( | |
| "active" | |
| ); | |
| } catch (error) { | |
| console.log( | |
| "Speech recognition:", | |
| error | |
| ); | |
| } | |
| } | |
| ); | |
| /* ========================= | |
| RESULT | |
| ========================= */ | |
| recognition.addEventListener( | |
| "result", | |
| event => { | |
| const transcript = | |
| event | |
| .results[0][0] | |
| .transcript; | |
| /* | |
| Add dictated text. | |
| */ | |
| const existing = | |
| getInputText( | |
| targetInput | |
| ); | |
| if (existing) { | |
| targetInput.innerText = | |
| existing + | |
| " " + | |
| transcript; | |
| } else { | |
| targetInput.innerText = | |
| transcript; | |
| } | |
| /* | |
| Update send button. | |
| */ | |
| updateButton( | |
| targetInput, | |
| targetSendButton | |
| ); | |
| /* | |
| Focus input. | |
| */ | |
| targetInput.focus(); | |
| } | |
| ); | |
| /* ========================= | |
| END | |
| ========================= */ | |
| recognition.addEventListener( | |
| "end", | |
| () => { | |
| button.classList.remove( | |
| "active" | |
| ); | |
| } | |
| ); | |
| /* ========================= | |
| ERROR | |
| ========================= */ | |
| recognition.addEventListener( | |
| "error", | |
| error => { | |
| console.log( | |
| "Dictation error:", | |
| error | |
| ); | |
| button.classList.remove( | |
| "active" | |
| ); | |
| } | |
| ); | |
| } | |
| /* ========================= | |
| ENABLE HOME DICTATION | |
| ========================= */ | |
| setupDictation( | |
| voice, | |
| input, | |
| send | |
| ); | |
| /* ========================= | |
| ENABLE CHAT DICTATION | |
| ========================= */ | |
| setupDictation( | |
| chatVoice, | |
| chatInput, | |
| chatSend | |
| ); | |
| /* ========================= | |
| INITIAL STATE | |
| ========================= */ | |
| clearInput( | |
| input | |
| ); | |
| clearInput( | |
| chatInput | |
| ); | |
| updateButton( | |
| input, | |
| send | |
| ); | |
| updateButton( | |
| chatInput, | |
| chatSend | |
| ); | |
| /* | |
| Make sure the header starts white. | |
| */ | |
| header.classList.remove( | |
| "dark" | |
| ); | |
| </script> | |
| </body> | |
| </html> |