PageSourceSearch

https://auroville.org/_auroville-chat/widget-20260925.js

js auroville.org collected 2026-10-01 10:45:57 UTC 8,496 bytes, 186 lines download raw bytes

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.