Reworking css

This commit is contained in:
tyler
2026-05-19 01:46:54 -04:00
parent 4b36a261c4
commit 7367e993be
+91 -57
View File
@@ -335,9 +335,9 @@ INTERACTIVE_CSS = '''
opacity: 0.55; opacity: 0.55;
animation: sxng-fade-in-up 0.3s ease-out forwards; animation: sxng-fade-in-up 0.3s ease-out forwards;
} }
.sxng-input-wrapper:focus-within { .sxng-input-wrapper:focus-within {
opacity: 1; opacity: 1;
color: var(--color-result-link, #5e81ac); color: var(--color-result-link, #5e81ac);
background: var(--color-base-background-hover, rgba(0,0,0,0.05)) !important; background: var(--color-base-background-hover, rgba(0,0,0,0.05)) !important;
} }
.sxng-model-select { .sxng-model-select {
@@ -507,7 +507,7 @@ CITATION_HELPER_JS = r'''
const re = /\[(\d{1,2}(?:\s*,\s*\d{1,2})*)\]/g; const re = /\[(\d{1,2}(?:\s*,\s*\d{1,2})*)\]/g;
let lastIdx = 0; let lastIdx = 0;
const matches = [...text.matchAll(re)]; const matches = [...text.matchAll(re)];
matches.forEach(match => { matches.forEach(match => {
if (match.index > lastIdx) { if (match.index > lastIdx) {
const s = document.createElement('span'); const s = document.createElement('span');
@@ -542,7 +542,7 @@ CITATION_HELPER_JS = r'''
}); });
lastIdx = match.index + match[0].length; lastIdx = match.index + match[0].length;
}); });
if (lastIdx < text.length) { if (lastIdx < text.length) {
const s = document.createElement('span'); const s = document.createElement('span');
s.className = 'sxng-chunk'; s.className = 'sxng-chunk';
@@ -636,13 +636,13 @@ INTERACTIVE_JS = r'''
} }
return btoa(bin); return btoa(bin);
}; };
let b64 = encodeB64(state); let b64 = encodeB64(state);
while (b64.length > 2000 && state.t.length > 2) { while (b64.length > 2000 && state.t.length > 2) {
state.t.splice(1, 2); // Delete in Q&A pairs state.t.splice(1, 2); // Delete in Q&A pairs
b64 = encodeB64(state); b64 = encodeB64(state);
} }
history.replaceState(null, null, '#ai=' + b64); history.replaceState(null, null, '#ai=' + b64);
} catch(e) {} } catch(e) {}
}; };
@@ -658,17 +658,17 @@ INTERACTIVE_JS = r'''
if (state.u && Array.isArray(state.u)) { if (state.u && Array.isArray(state.u)) {
urls = state.u; urls = state.u;
} }
conversation.turns = state.t.map(t => ({ conversation.turns = state.t.map(t => ({
role: t.r === 'u' ? 'user' : 'assistant', role: t.r === 'u' ? 'user' : 'assistant',
content: t.c.trim(), content: t.c.trim(),
ts: 0 ts: 0
})); }));
const injectCitations = (text) => { const injectCitations = (text) => {
return renderCitations(text, urls); return renderCitations(text, urls);
}; };
data.innerHTML = ''; data.innerHTML = '';
conversation.turns.forEach((turn, i) => { conversation.turns.forEach((turn, i) => {
if (turn.role === 'user') { if (turn.role === 'user') {
@@ -756,10 +756,10 @@ INTERACTIVE_JS = r'''
const handleAction = async (e) => { const handleAction = async (e) => {
if (e) e.preventDefault(); if (e) e.preventDefault();
const val = input.value.trim(); const val = input.value.trim();
conversation.turns.push({role: 'user', content: val, ts: Date.now()}); conversation.turns.push({role: 'user', content: val, ts: Date.now()});
updateState(); updateState();
const currentText = conversation.turns.slice(0, -1).slice(-6) const currentText = conversation.turns.slice(0, -1).slice(-6)
.map(t => (t.role === 'user' ? 'Q' : 'A') + ': ' + t.content) .map(t => (t.role === 'user' ? 'Q' : 'A') + ': ' + t.content)
.join('\\n\\n'); .join('\\n\\n');
@@ -782,7 +782,7 @@ INTERACTIVE_JS = r'''
const newCursor = document.createElement('span'); const newCursor = document.createElement('span');
newCursor.className = 'sxng-cursor'; newCursor.className = 'sxng-cursor';
data.appendChild(newCursor); data.appendChild(newCursor);
const synthesized = synthesizeQuery(q_init, val); const synthesized = synthesizeQuery(q_init, val);
let auxContext = null; let auxContext = null;
try { try {
@@ -799,7 +799,7 @@ INTERACTIVE_JS = r'''
} }
} }
} catch (err) {} } catch (err) {}
await startStream(val, currentText, auxContext); await startStream(val, currentText, auxContext);
updateState(); updateState();
} else { } else {
@@ -876,11 +876,44 @@ FRONTEND_JS_TEMPLATE = r"""
}; };
const box = document.getElementById('sxng-stream-box'); const box = document.getElementById('sxng-stream-box');
const data = document.getElementById('sxng-stream-data'); const data = document.getElementById('sxng-stream-data');
const wrapper = box.closest('.answer');
if (wrapper) wrapper.style.display = 'none'; // Move AI Overview outside #answers, place it before #results
(function relocateBox() {
const answersDiv = document.getElementById('answers');
if (!box || !answersDiv) return;
// Create our own container
const aiContainer = document.createElement('div');
aiContainer.id = 'ai-answers';
aiContainer.style.cssText = [
'background: var(--color-answer-background)',
'padding: 1rem',
'margin: 0 0 1rem 0',
'color: var(--color-answer-font)',
'border-radius: 8px',
'box-sizing: border-box',
'width: 100%'
].join('; ');
// Move our box into the new container
aiContainer.appendChild(box);
const resultsGrid = document.getElementById('results');
if (resultsGrid) {
// Insert as first child of #results grid so grid-area:answers applies
resultsGrid.insertBefore(aiContainer, resultsGrid.firstChild);
} else {
answersDiv.parentNode.insertBefore(aiContainer, answersDiv);
}
// Hide #answers entirely since our box is now elsewhere
answersDiv.style.display = 'none';
})();
let restored = false; let restored = false;
let isStreaming = false; let isStreaming = false;
__CITATION_HELPER_JS__ __CITATION_HELPER_JS__
(function applyIntentBadge() { (function applyIntentBadge() {
@@ -943,11 +976,10 @@ FRONTEND_JS_TEMPLATE = r"""
console.warn('[AI Answers] Stream already in progress, ignoring duplicate call'); console.warn('[AI Answers] Stream already in progress, ignoring duplicate call');
return; return;
} }
isStreaming = true; isStreaming = true;
try { try {
const ctx = auxContext || conversation.originalContext; const ctx = auxContext || conversation.originalContext;
if (wrapper) wrapper.style.display = '';
box.style.display = 'block'; box.style.display = 'block';
const controller = new AbortController(); const controller = new AbortController();
@@ -1002,6 +1034,11 @@ FRONTEND_JS_TEMPLATE = r"""
data.appendChild(cursor); data.appendChild(cursor);
} }
const streamContainer = document.createElement('div');
streamContainer.className = 'sxng-stream-container';
if (cursor) cursor.before(streamContainer);
else data.appendChild(streamContainer);
let buffer = ''; let buffer = '';
let fullText = ''; let fullText = '';
const flushBuffer = (force = false) => { const flushBuffer = (force = false) => {
@@ -1009,8 +1046,7 @@ FRONTEND_JS_TEMPLATE = r"""
if (force) { if (force) {
const fragment = renderCitations(buffer, urls); const fragment = renderCitations(buffer, urls);
if (cursor) cursor.before(fragment); streamContainer.appendChild(fragment);
else data.appendChild(fragment);
buffer = ''; buffer = '';
return; return;
} }
@@ -1025,12 +1061,12 @@ FRONTEND_JS_TEMPLATE = r"""
const s = document.createElement('span'); const s = document.createElement('span');
s.className = 'sxng-chunk'; s.className = 'sxng-chunk';
s.textContent = preText; s.textContent = preText;
cursor.before(s); streamContainer.appendChild(s);
} }
const citationText = match[0]; const citationText = match[0];
const fragment = renderCitations(citationText, urls); const fragment = renderCitations(citationText, urls);
cursor.before(fragment); streamContainer.appendChild(fragment);
buffer = buffer.substring(match.index + match[0].length); buffer = buffer.substring(match.index + match[0].length);
} }
@@ -1041,7 +1077,7 @@ FRONTEND_JS_TEMPLATE = r"""
const s = document.createElement('span'); const s = document.createElement('span');
s.className = 'sxng-chunk'; s.className = 'sxng-chunk';
s.textContent = buffer; s.textContent = buffer;
cursor.before(s); streamContainer.appendChild(s);
buffer = ''; buffer = '';
} }
} else { } else {
@@ -1050,7 +1086,7 @@ FRONTEND_JS_TEMPLATE = r"""
const s = document.createElement('span'); const s = document.createElement('span');
s.className = 'sxng-chunk'; s.className = 'sxng-chunk';
s.textContent = safeChunk; s.textContent = safeChunk;
cursor.before(s); streamContainer.appendChild(s);
} }
buffer = buffer.substring(openIdx); buffer = buffer.substring(openIdx);
@@ -1058,7 +1094,7 @@ FRONTEND_JS_TEMPLATE = r"""
const s = document.createElement('span'); const s = document.createElement('span');
s.className = 'sxng-chunk'; s.className = 'sxng-chunk';
s.textContent = buffer[0]; s.textContent = buffer[0];
cursor.before(s); streamContainer.appendChild(s);
buffer = buffer.substring(1); buffer = buffer.substring(1);
} }
} }
@@ -1120,11 +1156,9 @@ FRONTEND_JS_TEMPLATE = r"""
} }
} }
streamContainer.remove();
if (cursor) cursor.remove(); if (cursor) cursor.remove();
// Remove only the streamed chunk spans added during this stream
Array.from(data.querySelectorAll('.sxng-chunk')).forEach(node => node.remove());
const rendered = parseMarkdown(fullText.trim()); const rendered = parseMarkdown(fullText.trim());
const mdDiv = document.createElement('div'); const mdDiv = document.createElement('div');
mdDiv.className = 'sxng-md-content'; mdDiv.className = 'sxng-md-content';
@@ -1151,13 +1185,13 @@ FRONTEND_JS_TEMPLATE = r"""
console.error('[AI Answers] Fatal stream exception:', e); console.error('[AI Answers] Fatal stream exception:', e);
const errSpan = document.createElement('span'); const errSpan = document.createElement('span');
errSpan.style.cssText = 'color: #bf616a; font-weight: bold; display: block; margin-top: 0.5rem;'; errSpan.style.cssText = 'color: #bf616a; font-weight: bold; display: block; margin-top: 0.5rem;';
if (e.name === 'AbortError') { if (e.name === 'AbortError') {
errSpan.textContent = "⚠️ Connection to AI provider timed out."; errSpan.textContent = "⚠️ Connection to AI provider timed out.";
} else { } else {
errSpan.textContent = "⚠️ AI Widget encountered a fatal error. Check browser console."; errSpan.textContent = "⚠️ AI Widget encountered a fatal error. Check browser console.";
} }
if (data) { if (data) {
const cursor = data.querySelector('.sxng-cursor'); const cursor = data.querySelector('.sxng-cursor');
if (cursor) cursor.remove(); if (cursor) cursor.remove();
@@ -1379,7 +1413,7 @@ class SXNGPlugin(Plugin):
'content': str(ib.get('content') or '')[:2000], 'content': str(ib.get('content') or '')[:2000],
'attributes': ib.get('attributes', []) 'attributes': ib.get('attributes', [])
}) })
answers = [] answers = []
for a in list(raw_answers)[:2]: for a in list(raw_answers)[:2]:
ans_text = "" ans_text = ""
@@ -1389,7 +1423,7 @@ class SXNGPlugin(Plugin):
ans_text = str(a['answer']) ans_text = str(a['answer'])
if ans_text and 'id="sxng-stream-box"' not in ans_text and not ans_text.strip().startswith('<'): if ans_text and 'id="sxng-stream-box"' not in ans_text and not ans_text.strip().startswith('<'):
answers.append(ans_text) answers.append(ans_text)
return results, infoboxes, answers return results, infoboxes, answers
def init(self, app): def init(self, app):
@@ -1397,10 +1431,10 @@ class SXNGPlugin(Plugin):
def ai_auxiliary_search(): def ai_auxiliary_search():
if not self.api_key: if not self.api_key:
abort(403) abort(403)
data = request.json or {} data = request.json or {}
token = data.get('tk', '') token = data.get('tk', '')
# Token access control # Token access control
try: try:
ts, sig = token.rsplit('.', 1) ts, sig = token.rsplit('.', 1)
@@ -1420,13 +1454,13 @@ class SXNGPlugin(Plugin):
offset = data.get('offset', 0) offset = data.get('offset', 0)
if not query: if not query:
return jsonify({'results': []}) return jsonify({'results': []})
try: try:
from searx.search import SearchWithPlugins from searx.search import SearchWithPlugins
from searx.search.models import SearchQuery from searx.search.models import SearchQuery
from searx.query import RawTextQuery from searx.query import RawTextQuery
from searx.webadapter import get_engineref_from_category_list from searx.webadapter import get_engineref_from_category_list
preferences = getattr(request, 'preferences', None) preferences = getattr(request, 'preferences', None)
disabled_engines = preferences.engines.get_disabled() if preferences else [] disabled_engines = preferences.engines.get_disabled() if preferences else []
rtq = RawTextQuery(query, disabled_engines) rtq = RawTextQuery(query, disabled_engines)
@@ -1434,7 +1468,7 @@ class SXNGPlugin(Plugin):
category_list = [c.strip() for c in categories.split(',') if c.strip()] category_list = [c.strip() for c in categories.split(',') if c.strip()]
else: else:
category_list = categories or ['general'] category_list = categories or ['general']
enginerefs = get_engineref_from_category_list(category_list, disabled_engines) enginerefs = get_engineref_from_category_list(category_list, disabled_engines)
sq = SearchQuery( sq = SearchQuery(
query=rtq.getQuery(), query=rtq.getQuery(),
@@ -1444,19 +1478,19 @@ class SXNGPlugin(Plugin):
) )
search_obj = SearchWithPlugins(sq, request, user_plugins=[]) search_obj = SearchWithPlugins(sq, request, user_plugins=[])
result_container = search_obj.search() result_container = search_obj.search()
raw_results = result_container.get_ordered_results() raw_results = result_container.get_ordered_results()
raw_infoboxes = getattr(result_container, 'infoboxes', []) raw_infoboxes = getattr(result_container, 'infoboxes', [])
raw_answers = getattr(result_container, 'answers', []) raw_answers = getattr(result_container, 'answers', [])
results, infoboxes, answers = self._parse_aux_results(raw_results, raw_infoboxes, raw_answers) results, infoboxes, answers = self._parse_aux_results(raw_results, raw_infoboxes, raw_answers)
context_str, new_urls = self._assemble_context(results, infoboxes, answers, offset) context_str, new_urls = self._assemble_context(results, infoboxes, answers, offset)
return jsonify({ return jsonify({
'context': context_str, 'context': context_str,
'new_urls': new_urls, 'new_urls': new_urls,
'results': results, 'results': results,
'infoboxes': infoboxes, 'infoboxes': infoboxes,
'answers': answers, 'answers': answers,
'query': query 'query': query
@@ -1876,12 +1910,12 @@ class SXNGPlugin(Plugin):
"""Builds context string from normalized search data. Returns (context_str, urls).""" """Builds context string from normalized search data. Returns (context_str, urls)."""
context_parts = [] context_parts = []
result_urls = [] result_urls = []
knowledge_graph_lines = [] knowledge_graph_lines = []
for ib in infoboxes: for ib in infoboxes:
ib_name = ib.get('name', '') or ib.get('infobox', '') or ib.get('title', '') ib_name = ib.get('name', '') or ib.get('infobox', '') or ib.get('title', '')
ib_content = str(ib.get('content', '')).replace('\n', ' ').strip() ib_content = str(ib.get('content', '')).replace('\n', ' ').strip()
if ib_name: if ib_name:
parts = [f"INFOBOX [{ib_name}]:"] parts = [f"INFOBOX [{ib_name}]:"]
if ib_content: if ib_content:
@@ -1891,16 +1925,16 @@ class SXNGPlugin(Plugin):
attr_value = attr.get('value', '') attr_value = attr.get('value', '')
if attr_label and attr_value: if attr_label and attr_value:
parts.append(f" {attr_label}: {attr_value}") parts.append(f" {attr_label}: {attr_value}")
knowledge_graph_lines.append(" ".join(parts) if len(parts) == 2 else "\n".join(parts)) knowledge_graph_lines.append(" ".join(parts) if len(parts) == 2 else "\n".join(parts))
for ans_text in answers: for ans_text in answers:
if ans_text and not str(ans_text).startswith('<'): if ans_text and not str(ans_text).startswith('<'):
knowledge_graph_lines.append(f"ANSWER: {str(ans_text)[:300]}") knowledge_graph_lines.append(f"ANSWER: {str(ans_text)[:300]}")
if knowledge_graph_lines: if knowledge_graph_lines:
context_parts.append("KNOWLEDGE GRAPH:\n" + "\n".join(knowledge_graph_lines)) context_parts.append("KNOWLEDGE GRAPH:\n" + "\n".join(knowledge_graph_lines))
deep_lines = [] deep_lines = []
for i, r in enumerate(clean_results[:self.context_deep_count]): for i, r in enumerate(clean_results[:self.context_deep_count]):
url = r.get('url', '') url = r.get('url', '')
@@ -1916,10 +1950,10 @@ class SXNGPlugin(Plugin):
logger.debug(f"{PLUGIN_NAME}: falling back to snippet for [{idx}] {domain}") logger.debug(f"{PLUGIN_NAME}: falling back to snippet for [{idx}] {domain}")
content = str(r.get('content', '')).replace('\n', ' ').strip()[:800] content = str(r.get('content', '')).replace('\n', ' ').strip()[:800]
deep_lines.append(f"[{idx}] {domain}{date_str}: {title}: {content}") deep_lines.append(f"[{idx}] {domain}{date_str}: {title}: {content}")
if deep_lines: if deep_lines:
context_parts.append("DEEP SOURCES:\n" + "\n".join(deep_lines)) context_parts.append("DEEP SOURCES:\n" + "\n".join(deep_lines))
if self.context_shallow_count > 0: if self.context_shallow_count > 0:
shallow_lines = [] shallow_lines = []
start_idx = self.context_deep_count start_idx = self.context_deep_count
@@ -1931,10 +1965,10 @@ class SXNGPlugin(Plugin):
title = r.get('title', '').replace('\n', ' ').strip()[:60] title = r.get('title', '').replace('\n', ' ').strip()[:60]
idx = i + 1 + start_idx + offset idx = i + 1 + start_idx + offset
shallow_lines.append(f"[{idx}] {domain}: {title}") shallow_lines.append(f"[{idx}] {domain}: {title}")
if shallow_lines: if shallow_lines:
context_parts.append("SHALLOW SOURCES (headlines):\n" + "\n".join(shallow_lines)) context_parts.append("SHALLOW SOURCES (headlines):\n" + "\n".join(shallow_lines))
return "\n\n".join(context_parts), result_urls return "\n\n".join(context_parts), result_urls
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults: def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
@@ -1958,7 +1992,7 @@ class SXNGPlugin(Plugin):
raw_results = search.result_container.get_ordered_results() raw_results = search.result_container.get_ordered_results()
raw_infoboxes = getattr(search.result_container, 'infoboxes', []) raw_infoboxes = getattr(search.result_container, 'infoboxes', [])
raw_answers = getattr(search.result_container, 'answers', []) raw_answers = getattr(search.result_container, 'answers', [])
q_clean = search.search_query.query.strip() q_clean = search.search_query.query.strip()
clean_results, infoboxes, answers = self._parse_aux_results(raw_results, raw_infoboxes, raw_answers) clean_results, infoboxes, answers = self._parse_aux_results(raw_results, raw_infoboxes, raw_answers)
clean_results = self._enrich_results(clean_results, q_clean) clean_results = self._enrich_results(clean_results, q_clean)
@@ -1981,12 +2015,12 @@ class SXNGPlugin(Plugin):
detected_intent = _detect_intent(q_clean) detected_intent = _detect_intent(q_clean)
js_intent = safe_json(detected_intent) js_intent = safe_json(detected_intent)
b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8') b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8')
total_context_count = self.context_deep_count + self.context_shallow_count total_context_count = self.context_deep_count + self.context_shallow_count
raw_urls = [r.get('url', '') for r in clean_results[:total_context_count]] raw_urls = [r.get('url', '') for r in clean_results[:total_context_count]]
js_q = safe_json(q_clean) js_q = safe_json(q_clean)
js_lang = safe_json(lang) js_lang = safe_json(lang)
js_urls = safe_json(raw_urls) js_urls = safe_json(raw_urls)
@@ -2028,7 +2062,7 @@ class SXNGPlugin(Plugin):
.replace("__JS_Q__", js_q) .replace("__JS_Q__", js_q)
html_payload = f''' html_payload = f'''
<article id="sxng-stream-box" class="answer" style="display:none; margin-top: 0; margin-bottom: 0;"> <article id="sxng-stream-box" class="answer" style="display:none; margin: 0; padding: 0;">
<style> <style>
@keyframes sxng-fade-pulse {{ @keyframes sxng-fade-pulse {{
0%, 100% {{ opacity: 0.1; }} 0%, 100% {{ opacity: 0.1; }}
@@ -2092,4 +2126,4 @@ class SXNGPlugin(Plugin):
search.result_container.answers.add(results.types.Answer(answer=Markup(html_payload))) search.result_container.answers.add(results.types.Answer(answer=Markup(html_payload)))
except Exception as e: except Exception as e:
logger.error(f"{PLUGIN_NAME}: {e}") logger.error(f"{PLUGIN_NAME}: {e}")
return results return results