1(() => { 2 document.getElementById('auroville-chat-root')?.remove(); 3 4 const root = document.createElement('div'); 5 root.id = 'auroville-chat-root'; 6 root.innerHTML = ` 7 <button type="button" id="av-chat-launch" aria-label="Chat with Auroville">ð¬</button> 8 <section id="av-chat-box" hidden aria-label="Auroville information chat"> 9 <header>Auroville information <button type="button" id="av-chat-close" aria-label="Close chat">Ã</button></header> 10 <div id="av-chat-log" role="log" aria-live="polite"><p>Ask me about information on auroville.org.</p></div> 11 <p id="av-chat-notice">This chatbot is in development and can make mistakes. Please check the source page for important details.</p> 12 <div id="av-chat-controls"> 13 <input id="av-chat-input" type="text" autocomplete="off" placeholder="Ask a questionâ¦" aria-label="Question"> 14 <button type="button" id="av-chat-send">Send</button> 15 </div> 16 </section>`; 17 18 const css = document.createElement('style'); 19 css.textContent = ` 20 #auroville-chat-root{font-family:'Open Sans',system-ui,sans-serif;position:fixed;right:20px;bottom:20px;z-index:2147483647} 21 #av-chat-launch{border:0;border-radius:50%;width:54px;height:54px;background:#9b543e;color:#ffe4bb;font-size:24px;box-shadow:0 3px 14px #0004;cursor:pointer} 22 #av-chat-box{width:min(330px,calc(100vw - 32px));height:430px;background:#fff;border:1px solid #e5d0bd;border-radius:14px;box-shadow:0 5px 25px #0003;overflow:hidden;margin-bottom:10px;display:flex;flex-direction:column} 23 #av-chat-box[hidden]{display:none} 24 #av-chat-box header{background:#9b543e;color:#ffe4bb;padding:13px;font-weight:600} 25 #av-chat-close{float:right;background:none;border:0;color:#ffe4bb;font-size:20px;cursor:pointer} 26 #av-chat-log{flex:1;min-height:0;overflow:auto;padding:12px;font-size:14px} 27 #av-chat-log p{background:#fff7eb;border-radius:10px;padding:9px;margin:7px 0;white-space:pre-wrap;overflow-wrap:anywhere} 28 #av-chat-log a{color:#8a422f;text-decoration:underline} 29 #av-chat-log a.av-chat-source,#av-chat-log a.av-chat-map{font-weight:700;cursor:pointer;text-underline-offset:2px} 30 #av-chat-notice{margin:0;padding:6px 10px;background:#fff7eb;color:#76564a;font-size:11px;line-height:1.35;border-top:1px solid #e5d0bd} 31 #av-chat-controls{display:flex;border-top:1px solid #e5d0bd;padding:8px;gap:6px} 32 #av-chat-input{min-width:0;flex:1;padding:8px;border:1px solid #d6b7a0;border-radius:8px} 33 #av-chat-send{background:#9b543e;color:#fff;border:0;border-radius:8px;padding:0 12px;cursor:pointer} 34 #av-chat-send:disabled{opacity:.65;cursor:wait}`; 35 document.head.appendChild(css); 36 document.body.appendChild(root); 37 38 const launch = root.querySelector('#av-chat-launch'); 39 const panel = root.querySelector('#av-chat-box'); 40 const close = root.querySelector('#av-chat-close'); 41 const log = root.querySelector('#av-chat-log'); 42 const input = root.querySelector('#av-chat-input'); 43 const sendButton = root.querySelector('#av-chat-send'); 44 let sending = false; 45 const storageKey = 'auroville-chat-state-v1'; 46 let chatState = {open: false, draft: '', messages: [], pending: ''}; 47 try { 48 const saved = JSON.parse(sessionStorage.getItem(storageKey) || 'null'); 49 if (saved && typeof saved === 'object') { 50 chatState.open = saved.open === true; 51 chatState.draft = typeof saved.draft === 'string' ? saved.draft.slice(0, 2000) : ''; 52 chatState.messages = Array.isArray(saved.messages) ? saved.messages.slice(-30).filter(item => item && ['You', 'Auroville'].includes(item.who) && typeof item.value === 'string') : []; 53 chatState.pending = typeof saved.pending === 'string' ? saved.pending.slice(0, 2000) : ''; 54 } 55 } catch {} 56 function saveState() { 57 try { sessionStorage.setItem(storageKey, JSON.stringify(chatState)); } catch {} 58 } 59 60 function message(who, value, persist = true) { 61 const line = document.createElement('p'); 62 if (who) { 63 const label = document.createElement('b'); 64 label.textContent = `${who}: `; 65 line.appendChild(label); 66 } 67 const linkPattern = /https?:\/\/[^\s]+|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}/g; 68 let offset = 0; 69 for (const match of value.matchAll(linkPattern)) { 70 line.appendChild(document.createTextNode(value.slice(offset, match.index))); 71 const raw = match[0]; 72 const cleaned = raw.replace(/[.,;)]+$/, ''); 73 const link = document.createElement('a'); 74 link.href = cleaned.includes('@') && !cleaned.startsWith('http') ? `mailto:${cleaned}` : cleaned; 75 const lineStart = value.lastIndexOf('\n', match.index - 1) + 1; 76 const isSource = /^\s*Source:\s*$/.test(value.slice(lineStart, match.index)); 77 const isMap = /https?:\/\/(?:www\.)?(?:google\.com\/maps|openstreetmap\.org\/)/.test(cleaned); 78 link.textContent = isSource ? 'Open source page' : isMap ? 'Open map' : cleaned; 79 if (isSource || isMap) link.className = isSource ? 'av-chat-source' : 'av-chat-map'; 80 if (isSource || isMap) { 81 link.addEventListener('click', event => { 82 event.preventDefault(); 83 event.stopPropagation(); 84 window.location.assign(link.href); 85 }); 86 } else { 87 link.target = '_blank'; 88 } 89 link.rel = 'noopener noreferrer'; 90 line.appendChild(link); 91 line.appendChild(document.createTextNode(raw.slice(cleaned.length))); 92 offset = match.index + raw.length; 93 } 94 line.appendChild(document.createTextNode(value.slice(offset))); 95 log.appendChild(line); 96 log.scrollTop = log.scrollHeight; 97 if (persist) { 98 chatState.messages.push({who, value}); 99 chatState.messages = chatState.messages.slice(-30); 100 saveState(); 101 } 102 return line; 103 } 104 if (chatState.messages.length) { 105 log.replaceChildren();
106 chatState.messages.forEach(item => message(item.who, item.value, false)); 107 } 108 if (chatState.pending) { 109 chatState.draft = chatState.pending; 110 chatState.pending = ''; 111 message('Auroville', 'The page reconnected while I was answering. Your question is ready to send again.'); 112 } 113 input.value = chatState.draft; 114 panel.hidden = !chatState.open; 115 launch.hidden = chatState.open; 116 saveState(); 117 input.addEventListener('input', () => { 118 chatState.draft = input.value; 119 saveState(); 120 }); 121 122 async function send() { 123 const question = input.value.trim(); 124 if (!question || sending) return; 125 const history = chatState.messages.filter(item => item.who === 'You').slice(-4).map(item => item.value); 126 const previousSource = chatState.messages.slice().reverse().find(item => item.who === 'Auroville' && /Source:\s+https:\/\/auroville\.org\//.test(item.value)); 127 const source = previousSource?.value.match(/Source:\s+(https:\/\/auroville\.org\/[^\s]+)/)?.[1] || ''; 128 sending = true; 129 sendButton.disabled = true; 130 input.value = ''; 131 chatState.draft = ''; 132 chatState.pending = question; 133 saveState(); 134 message('You', question); 135 const pending = message('Auroville', 'Thinkingâ¦', false); 136 try { 137 const response = await fetch('/_auroville-chat/api/chat', { 138 method: 'POST', 139 headers: {'Content-Type': 'application/json'}, 140 body: JSON.stringify({question, history, source}) 141 }); 142 if (!response.ok) throw new Error('Chat request failed'); 143 const data = await response.json(); 144 pending.remove(); 145 message('Auroville', data.answer); 146 } catch { 147 pending.remove(); 148 message('Auroville', 'Sorry, the chat is temporarily unavailable.'); 149 } finally { 150 chatState.pending = ''; 151 saveState(); 152 sending = false; 153 sendButton.disabled = false; 154 input.focus(); 155 } 156 } 157 158 launch.addEventListener('click', event => { 159 event.preventDefault(); 160 panel.hidden = false; 161 launch.hidden = true; 162 chatState.open = true; 163 saveState(); 164 input.focus(); 165 }); 166 close.addEventListener('click', event => { 167 event.preventDefault(); 168 panel.hidden = true; 169 launch.hidden = false; 170 chatState.open = false; 171 saveState(); 172 }); 173 sendButton.addEventListener('click', event => { 174 event.preventDefault(); 175 send(); 176 }); 177 input.addEventListener('keydown', event => { 178 if (event.key === 'Enter') { 179 event.preventDefault(); 180 event.stopPropagation(); 181 send(); 182 } 183 }); 184 root.addEventListener('submit', event => event.preventDefault(), true); 185 root.addEventListener('click', event => event.stopPropagation()); 186})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.