Making the code base easy to read and maintain and making it a oneline install
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
@keyframes sxng-fade-in-up {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.sxng-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 0;
|
||||
animation: sxng-fade-in-up 0.5s ease-out forwards;
|
||||
}
|
||||
.sxng-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: var(--color-sidebar-bg, #424247);
|
||||
color: var(--color-search-url, #bbb);
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.sxng-btn:hover {
|
||||
background: var(--color-search-url, #303033);
|
||||
color: var(--color-sidebar-bg, #bbb);
|
||||
}
|
||||
.sxng-btn svg { width: 18px; height: 18px; fill: currentColor; }
|
||||
.sxng-input-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
.sxng-input {
|
||||
width: 100%;
|
||||
height: -webkit-fill-available;
|
||||
background: var(--color-sidebar-bg, #424247);
|
||||
border: none;
|
||||
color: var(--color-base-font, #cdd6f4);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 0.78em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.sxng-input:focus { outline: none; }
|
||||
.sxng-input::placeholder { color: var(--color-base-font, #333); opacity: 0.35; }
|
||||
.sxng-input-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: var(--color-result-link, #5e81ac);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.sxng-input:focus + .sxng-input-line { width: 100%; }
|
||||
.sxng-user-msg {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
margin: 0.75rem 0 0.75rem auto;
|
||||
padding: 0.25rem 0.6rem 0.25rem 0;
|
||||
border-right: 2px solid var(--color-result-link, #5e81ac);
|
||||
text-align: right;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
opacity: 0.55;
|
||||
animation: sxng-fade-in-up 0.3s ease-out forwards;
|
||||
}
|
||||
.sxng-input-wrapper:focus-within {
|
||||
opacity: 1;
|
||||
color: var(--color-result-link, #5e81ac);
|
||||
background: var(--color-base-background-hover, rgba(0,0,0,0.05)) !important;
|
||||
}
|
||||
.sxng-model-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: url("data:image/svg+xml;charset=UTF-8,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%0A%3Cg%20fill%3D%22%23aaa%22%3E%0A%3Cpolygon%20points%3D%22128%2C192%20256%2C320%20384%2C192%22%2F%3E%3C%2Fg%3E%0A%3C%2Fsvg%3E") calc(100% + 2rem) / 1rem no-repeat content-box border-box;
|
||||
background-color: #424247;
|
||||
text-overflow: ellipsis;
|
||||
border-width: 0 2rem 0 0;
|
||||
border-color: transparent;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
height: 32px;
|
||||
color: var(--color-search-url, #bbb);
|
||||
font-size: .9rem;
|
||||
padding: 1px 10px 1px 10px !important;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
max-width: 8rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.sxng-model-select:hover {
|
||||
background-color: #303033;
|
||||
color: var(--color-search-url, #bbb);
|
||||
}
|
||||
.sxng-reasoning {
|
||||
margin: 0.5rem 0; padding: 0.5rem;
|
||||
border-left: 2px solid var(--color-result-link, #5e81ac);
|
||||
background: var(--color-base-background-hover, rgba(0,0,0,0.03));
|
||||
font-size: 0.85rem; opacity: 0.7; transition: opacity 0.2s;
|
||||
}
|
||||
.sxng-reasoning:hover { opacity: 1; }
|
||||
.sxng-reasoning summary { cursor: pointer; font-weight: bold; color: var(--color-result-link, #5e81ac); }
|
||||
.sxng-thought-content { margin-top: 0.5rem; white-space: pre-wrap; font-family: monospace; }
|
||||
.sxng-citation-footer {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--color-sidebar-bg, #424247);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 0.75rem;
|
||||
}
|
||||
.sxng-citation-item a {
|
||||
font-size: 0.75em;
|
||||
color: var(--color-result-link, #5e81ac);
|
||||
text-decoration: none;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.sxng-citation-item a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div id="sxng-footer" class="sxng-footer" style="display:none;">
|
||||
<button class="sxng-btn" id="btn-copy" title="Copy to clipboard">
|
||||
<svg viewBox="0 0 24 24"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1M19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5M19 21H8V7H19V21Z"/></svg>
|
||||
</button>
|
||||
<button class="sxng-btn" id="btn-regen" title="Regenerate answer">
|
||||
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4.01 7.58 4.01 12C4.01 16.42 7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12C6 8.69 8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z"/></svg>
|
||||
</button>
|
||||
<form id="sxng-action-form" class="sxng-input-wrapper" onsubmit="event.preventDefault();">
|
||||
<input type="text" id="sxng-action-input" class="sxng-input" placeholder="Ask..." aria-label="Ask follow-up" autocomplete="off">
|
||||
<div class="sxng-input-line"></div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
// === 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(/^<think>([\s\S]*?)<\/think>\s*/);
|
||||
if (thinkMatch) {
|
||||
const cursorTh = data.querySelector('.sxng-cursor');
|
||||
const details = document.createElement('details');
|
||||
details.className = 'sxng-reasoning';
|
||||
details.innerHTML = '<summary>Thought Process</summary>';
|
||||
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 = '<svg viewBox="0 0 24 24" style="color:#a3be8c;"><path d="M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z"/></svg>';
|
||||
setTimeout(() => btn.innerHTML = originalContent, 2000);
|
||||
};
|
||||
|
||||
document.getElementById('btn-regen').onclick = async () => {
|
||||
data.innerHTML = '<span class="sxng-cursor"></span>';
|
||||
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';
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user