// === FRONTEND_JS_TEMPLATE === (async () => { const is_interactive = __IS_INTERACTIVE__; const q_init = __JS_Q__; const lang_init = __JS_LANG__; let urls = __JS_URLS__; const b64_init = __B64_CONTEXT__; const tk_init = __TK__; const script_root = __SCRIPT_ROOT__; const model_init = __MODEL_INIT__; const conversation = { originalQuery: q_init, originalContext: new TextDecoder().decode(Uint8Array.from(atob(b64_init), c => c.charCodeAt(0))), originalSources: [...urls], turns: [{role: 'user', content: q_init, ts: Date.now()}] }; const box = document.getElementById('sxng-stream-box'); const data = document.getElementById('sxng-stream-data'); const wrapper = box.closest('.answer'); if (wrapper) wrapper.style.display = 'none'; let restored = false; let isStreaming = false; __CITATION_HELPER_JS__ __INTERACTIVE_JS_INIT__ function synthesizeQuery(original, followup) { const cleanOrig = original.replace(/^(what|how|why|when|where|who|which|is|are|can|does|do)(\s+(is|are|do|does|can|to|a|an|the))?\s+/i, ''); const origWords = cleanOrig.split(' ').slice(0, 12); return `${origWords.join(' ')} ${followup}`.trim(); } __STREAM_FN_SIG__ { if (isStreaming) { console.warn('[AI Answers] Stream already in progress, ignoring duplicate call'); return; } isStreaming = true; try { const ctx = auxContext || conversation.originalContext; if (wrapper) wrapper.style.display = ''; box.style.display = 'block'; const controller = new AbortController(); let timeoutId = setTimeout(() => controller.abort(), 60000); const finalQ = __STREAM_Q__; const _selMdl = (document.getElementById('sxng-model-select') || {value: ''}).value; const bodyObj = { q: finalQ, lang: lang_init, context: ctx, tk: tk_init, model: _selMdl__STREAM_BODY__ }; const res = await fetch(script_root + '/ai-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyObj), signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) { const errSpan = document.createElement('span'); errSpan.style.color = '#bf616a'; errSpan.textContent = "Error: " + res.statusText; data.appendChild(errSpan); return; } const respJson = await res.json(); if (respJson.error) { const cursorErr = data.querySelector('.sxng-cursor'); if (cursorErr) cursorErr.remove(); const errSpan = document.createElement('span'); errSpan.style.color = '#bf616a'; errSpan.textContent = "⚠️ " + respJson.error; data.appendChild(errSpan); return; } const fullText = (respJson.text || '').trim(); if (!fullText) { const cursorErr = data.querySelector('.sxng-cursor'); if (cursorErr) cursorErr.remove(); const errSpan = document.createElement('span'); errSpan.style.color = '#bf616a'; errSpan.textContent = 'No response received. Check API configuration and server logs.'; data.appendChild(errSpan); return; } let mainText = fullText; const thinkMatch = mainText.match(/^([\s\S]*?)<\/think>\s*/); if (thinkMatch) { const cursorTh = data.querySelector('.sxng-cursor'); const details = document.createElement('details'); details.className = 'sxng-reasoning'; details.innerHTML = 'Thought Process'; const thoughtDiv = document.createElement('div'); thoughtDiv.className = 'sxng-thought-content'; thoughtDiv.textContent = thinkMatch[1]; details.appendChild(thoughtDiv); if (cursorTh) cursorTh.before(details); else data.appendChild(details); mainText = mainText.substring(thinkMatch[0].length); } let cursor = data.querySelector('.sxng-cursor'); if (!cursor) { cursor = document.createElement('span'); cursor.className = 'sxng-cursor'; data.appendChild(cursor); } let buffer = ''; const flushBuffer = (force = false) => { if (!buffer) return; if (force) { const fragment = renderCitations(buffer, urls); if (cursor) cursor.before(fragment); else data.appendChild(fragment); buffer = ''; return; } while (true) { const match = buffer.match(/(\[\d+(?:,\s*\d+)*\])/); if (!match) break; const preText = buffer.substring(0, match.index); if (preText) { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = preText; cursor.before(s); } const citationText = match[0]; const fragment = renderCitations(citationText, urls); cursor.before(fragment); buffer = buffer.substring(match.index + match[0].length); } const openIdx = buffer.lastIndexOf('['); if (openIdx === -1) { if (buffer) { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = buffer; cursor.before(s); buffer = ''; } } else { const safeChunk = buffer.substring(0, openIdx); if (safeChunk) { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = safeChunk; cursor.before(s); } buffer = buffer.substring(openIdx); if (buffer.length > 50) { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = buffer[0]; cursor.before(s); buffer = buffer.substring(1); } } }; let twPos = 0; const twBatch = 4; await new Promise(resolve => { function twTick() { if (twPos >= mainText.length) { flushBuffer(true); resolve(); return; } const end = Math.min(twPos + twBatch, mainText.length); buffer += mainText.substring(twPos, end); twPos = end; flushBuffer(false); setTimeout(twTick, 8); } twTick(); }); if (cursor) cursor.remove(); let last = data.lastChild; while (last) { if (last.textContent && last.textContent.trim().length === 0) { const prev = last.previousSibling; last.remove(); last = prev; } else { if (last.textContent) last.textContent = last.textContent.trimEnd(); break; } } renderCitationFooter(mainText, urls, data); const collectedResponse = mainText; __INTERACTIVE_JS_COMPLETE__ if (collectedResponse) { conversation.turns.push({role: 'assistant', content: collectedResponse.trim(), ts: Date.now()}); } // Save state if this was an initial generation or a regeneration if (arguments.length === 0 && typeof updateState === 'function') { updateState(); } } catch (e) { console.error('[AI Answers] Fatal stream exception:', e); const errSpan = document.createElement('span'); errSpan.style.cssText = 'color: #bf616a; font-weight: bold; display: block; margin-top: 0.5rem;'; if (e.name === 'AbortError') { errSpan.textContent = "⚠️ Connection to AI provider timed out."; } else { errSpan.textContent = "⚠️ AI Widget encountered a fatal error. Check browser console."; } if (data) { const cursor = data.querySelector('.sxng-cursor'); if (cursor) cursor.remove(); data.appendChild(errSpan); } } finally { isStreaming = false; } } if (!restored) startStream(); })(); // === CITATION_HELPER_JS === function renderCitations(text, urls) { const fragment = document.createDocumentFragment(); const re = /\[(\d{1,2}(?:\s*,\s*\d{1,2})*)\]/g; let lastIdx = 0; const matches = [...text.matchAll(re)]; matches.forEach(match => { if (match.index > lastIdx) { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = text.substring(lastIdx, match.index); fragment.appendChild(s); } match[1].split(/\s*,\s*/).forEach(n => { const idx = parseInt(n.trim()); if (idx >= 1 && idx <= urls.length) { const url = urls[idx-1]; if (url) { const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.style.cssText = 'text-decoration:none;color:var(--color-result-link);font-weight:bold;'; a.textContent = `[${n.trim()}]`; a.className = 'sxng-chunk'; fragment.appendChild(a); } else { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = `[${n.trim()}]`; fragment.appendChild(s); } } else { const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = `[${n.trim()}]`; fragment.appendChild(s); } }); lastIdx = match.index + match[0].length; }); if (lastIdx < text.length) { const s = document.createElement('span'); s.className = 'sxng-chunk'; // Preserve whitespace by not trimming s.textContent = text.substring(lastIdx); fragment.appendChild(s); } return fragment; } function renderCitationFooter(textContent, urls, container) { const re = /\[(\d{1,2}(?:\s*,\s*\d{1,2})*)\]/g; const usedIndices = new Set(); let m; while ((m = re.exec(textContent)) !== null) { m[1].split(/\s*,\s*/).forEach(n => { const idx = parseInt(n.trim()); if (idx >= 1 && idx <= urls.length && urls[idx - 1]) { usedIndices.add(idx); } }); } if (usedIndices.size === 0) return; const sorted = [...usedIndices].sort((a, b) => a - b); const footer = document.createElement('div'); footer.className = 'sxng-citation-footer'; sorted.forEach(n => { const url = urls[n - 1]; if (!url) return; let domain; try { domain = new URL(url).hostname.replace('www.', ''); } catch(e) { domain = url; } const item = document.createElement('span'); item.className = 'sxng-citation-item'; const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.textContent = `[${n}] ${domain}`; item.appendChild(a); footer.appendChild(item); }); container.appendChild(footer); } // === INTERACTIVE_JS === const footer = document.getElementById('sxng-footer'); const input = document.getElementById('sxng-action-input'); if (typeof model_init !== 'undefined' && model_init) { const _ms = document.getElementById('sxng-model-select'); if (_ms) { const _o = document.createElement('option'); _o.value = model_init; _o.textContent = model_init; _o.selected = true; _ms.appendChild(_o); } } if (window.getComputedStyle && box) { try { const docStyles = getComputedStyle(document.documentElement); let accent = docStyles.getPropertyValue('--color-result-link').trim(); if (!accent) { const a = document.createElement('a'); document.body.appendChild(a); accent = getComputedStyle(a).color; document.body.removeChild(a); } if (accent) { box.style.setProperty('--color-result-link', accent); box.style.setProperty('--sxng-ai-accent', accent); } } catch(e) {} } // conversation saved as base64 URL fragment. const updateState = () => { try { let state = { t: conversation.turns.map(t => ({ r: t.role === 'user' ? 'u' : 'a', c: t.content.replace(/\s+/g, ' ').trim() })), u: urls }; const encodeB64 = (obj) => { const u8 = new TextEncoder().encode(JSON.stringify(obj)); let bin = ''; // Use a loop to avoid RangeError: Maximum call stack size exceeded for (let i = 0; i < u8.byteLength; i++) { bin += String.fromCharCode(u8[i]); } return btoa(bin); }; let b64 = encodeB64(state); while (b64.length > 2000 && state.t.length > 2) { state.t.splice(1, 2); // Delete in Q&A pairs b64 = encodeB64(state); } history.replaceState(null, null, '#ai=' + b64); } catch(e) {} }; if (location.hash.includes('ai=')) { try { const b64 = location.hash.split('ai=')[1]; const uint8 = new Uint8Array(atob(b64).split('').map(c => c.charCodeAt(0))); const json = new TextDecoder().decode(uint8); const state = JSON.parse(json); if (state.t && state.t.length > 0) { // Restore URLs for citation indexing if (state.u && Array.isArray(state.u)) { urls = state.u; } conversation.turns = state.t.map(t => ({ role: t.r === 'u' ? 'user' : 'assistant', content: t.c.trim(), ts: 0 })); const injectCitations = (text) => { return renderCitations(text, urls); }; data.innerHTML = ''; conversation.turns.forEach((turn, i) => { if (turn.role === 'user') { if (turn.content !== conversation.originalQuery) { const u = document.createElement('span'); u.className = 'sxng-user-msg'; u.textContent = turn.content; data.appendChild(u); const clr = document.createElement('div'); clr.style.clear = 'both'; data.appendChild(clr); } } else { data.appendChild(injectCitations(turn.content)); } }); box.style.display = 'block'; if(wrapper) wrapper.style.display = ''; if(footer && is_interactive) footer.style.display = 'flex'; restored = true; } } catch(e) { console.warn('Restore failed', e); } } document.getElementById('btn-copy').onclick = async (e) => { const btn = e.currentTarget; const originalContent = btn.innerHTML; const text = Array.from(data.childNodes) .filter(n => n.nodeType === 3 || n.tagName === 'SPAN') .map(n => n.textContent) .join(''); await navigator.clipboard.writeText(text); btn.innerHTML = ''; setTimeout(() => btn.innerHTML = originalContent, 2000); }; document.getElementById('btn-regen').onclick = async () => { data.innerHTML = ''; footer.style.display = 'none'; if (conversation.turns.length > 0 && conversation.turns[conversation.turns.length - 1].role === 'assistant') { conversation.turns.pop(); } updateState(); if (conversation.turns.length <= 1) { await startStream(); } else { const val = conversation.turns[conversation.turns.length - 1].content; const currentText = conversation.turns.slice(0, -1).slice(-6) .map(t => (t.role === 'user' ? 'Q' : 'A') + ': ' + t.content) .join('\\n\\n'); await startStream(val, currentText); } updateState(); }; const handleAction = async (e) => { if (e) e.preventDefault(); const val = input.value.trim(); conversation.turns.push({role: 'user', content: val, ts: Date.now()}); updateState(); const currentText = conversation.turns.slice(0, -1).slice(-6) .map(t => (t.role === 'user' ? 'Q' : 'A') + ': ' + t.content) .join('\\n\\n'); input.value = ''; input.blur(); footer.style.display = 'none'; if (val) { const cursor = data.querySelector('.sxng-cursor'); if (cursor) cursor.remove(); const userMsg = document.createElement('span'); userMsg.className = 'sxng-user-msg'; userMsg.textContent = val; data.appendChild(userMsg); const clr = document.createElement('div'); clr.style.clear = 'both'; data.appendChild(clr); const newCursor = document.createElement('span'); newCursor.className = 'sxng-cursor'; data.appendChild(newCursor); const synthesized = synthesizeQuery(q_init, val); let auxContext = null; try { const auxData = await fetch(script_root + '/ai-auxiliary-search', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({query: synthesized, lang: lang_init, offset: urls.length, tk: tk_init}) }).then(r => r.json()); if (auxData.context) { const originalBackground = conversation.originalContext.substring(0, 1500); auxContext = `FRESH SOURCES (most relevant):\\n${auxData.context}\\n\\nBACKGROUND (for reference):\\n${originalBackground}`; if (auxData.new_urls && Array.isArray(auxData.new_urls)) { urls = urls.concat(auxData.new_urls); } } } catch (err) {} await startStream(val, currentText, auxContext); updateState(); } else { const cursor = data.querySelector('.sxng-cursor'); if (cursor) cursor.remove(); data.appendChild(document.createElement('br')); data.appendChild(document.createElement('br')); const newCursor = document.createElement('span'); newCursor.className = 'sxng-cursor'; data.appendChild(newCursor); await startStream("Continue", currentText); updateState(); } }; document.getElementById('sxng-action-form').onsubmit = handleAction; input.onfocus = () => { setTimeout(() => { input.scrollIntoView({behavior: 'smooth', block: 'center'}); }, 300); }; (function fetchModels() { const _msel2 = document.getElementById('sxng-model-select'); if (!_msel2) return; const _modelsUrl = script_root + '/ai-models?tk=' + encodeURIComponent(tk_init); console.log('[AI Answers] Fetching models from', _modelsUrl); fetch(_modelsUrl) .then(r => r.ok ? r.json() : Promise.reject('HTTP ' + r.status)) .then(d => { const models = (d && d.models && d.models.length > 0) ? d.models : [model_init]; const _cur = _msel2.value || model_init; _msel2.innerHTML = ''; models.forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = m; if (m === _cur) o.selected = true; _msel2.appendChild(o); }); _msel2.style.display = 'inline-block'; }) .catch(() => { if (model_init) { const o = document.createElement('option'); o.value = model_init; o.textContent = model_init; o.selected = true; _msel2.appendChild(o); _msel2.style.display = 'inline-block'; } }); })();