From 7367e993be96ecc848d8e6720eca4e4b571d5075 Mon Sep 17 00:00:00 2001 From: tyler Date: Tue, 19 May 2026 01:46:54 -0400 Subject: [PATCH] Reworking css --- ollama_answers.py | 148 ++++++++++++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 57 deletions(-) diff --git a/ollama_answers.py b/ollama_answers.py index 83ac20d..d38e940 100644 --- a/ollama_answers.py +++ b/ollama_answers.py @@ -335,9 +335,9 @@ INTERACTIVE_CSS = ''' 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); + .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 { @@ -507,7 +507,7 @@ CITATION_HELPER_JS = r''' 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'); @@ -542,7 +542,7 @@ CITATION_HELPER_JS = r''' }); lastIdx = match.index + match[0].length; }); - + if (lastIdx < text.length) { const s = document.createElement('span'); s.className = 'sxng-chunk'; @@ -636,13 +636,13 @@ INTERACTIVE_JS = r''' } 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) {} }; @@ -658,17 +658,17 @@ INTERACTIVE_JS = r''' 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') { @@ -756,10 +756,10 @@ INTERACTIVE_JS = r''' 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'); @@ -782,7 +782,7 @@ INTERACTIVE_JS = r''' const newCursor = document.createElement('span'); newCursor.className = 'sxng-cursor'; data.appendChild(newCursor); - + const synthesized = synthesizeQuery(q_init, val); let auxContext = null; try { @@ -799,7 +799,7 @@ INTERACTIVE_JS = r''' } } } catch (err) {} - + await startStream(val, currentText, auxContext); updateState(); } else { @@ -876,11 +876,44 @@ FRONTEND_JS_TEMPLATE = r""" }; 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'; + + // 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 isStreaming = false; - + __CITATION_HELPER_JS__ (function applyIntentBadge() { @@ -943,11 +976,10 @@ FRONTEND_JS_TEMPLATE = r""" 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(); @@ -1002,6 +1034,11 @@ FRONTEND_JS_TEMPLATE = r""" 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 fullText = ''; const flushBuffer = (force = false) => { @@ -1009,8 +1046,7 @@ FRONTEND_JS_TEMPLATE = r""" if (force) { const fragment = renderCitations(buffer, urls); - if (cursor) cursor.before(fragment); - else data.appendChild(fragment); + streamContainer.appendChild(fragment); buffer = ''; return; } @@ -1025,12 +1061,12 @@ FRONTEND_JS_TEMPLATE = r""" const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = preText; - cursor.before(s); + streamContainer.appendChild(s); } const citationText = match[0]; const fragment = renderCitations(citationText, urls); - cursor.before(fragment); + streamContainer.appendChild(fragment); buffer = buffer.substring(match.index + match[0].length); } @@ -1041,7 +1077,7 @@ FRONTEND_JS_TEMPLATE = r""" const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = buffer; - cursor.before(s); + streamContainer.appendChild(s); buffer = ''; } } else { @@ -1050,7 +1086,7 @@ FRONTEND_JS_TEMPLATE = r""" const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = safeChunk; - cursor.before(s); + streamContainer.appendChild(s); } buffer = buffer.substring(openIdx); @@ -1058,7 +1094,7 @@ FRONTEND_JS_TEMPLATE = r""" const s = document.createElement('span'); s.className = 'sxng-chunk'; s.textContent = buffer[0]; - cursor.before(s); + streamContainer.appendChild(s); buffer = buffer.substring(1); } } @@ -1120,11 +1156,9 @@ FRONTEND_JS_TEMPLATE = r""" } } + streamContainer.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 mdDiv = document.createElement('div'); mdDiv.className = 'sxng-md-content'; @@ -1151,13 +1185,13 @@ FRONTEND_JS_TEMPLATE = r""" 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(); @@ -1379,7 +1413,7 @@ class SXNGPlugin(Plugin): 'content': str(ib.get('content') or '')[:2000], 'attributes': ib.get('attributes', []) }) - + answers = [] for a in list(raw_answers)[:2]: ans_text = "" @@ -1389,7 +1423,7 @@ class SXNGPlugin(Plugin): ans_text = str(a['answer']) if ans_text and 'id="sxng-stream-box"' not in ans_text and not ans_text.strip().startswith('<'): answers.append(ans_text) - + return results, infoboxes, answers def init(self, app): @@ -1397,10 +1431,10 @@ class SXNGPlugin(Plugin): def ai_auxiliary_search(): if not self.api_key: abort(403) - + data = request.json or {} token = data.get('tk', '') - + # Token access control try: ts, sig = token.rsplit('.', 1) @@ -1420,13 +1454,13 @@ class SXNGPlugin(Plugin): offset = data.get('offset', 0) if not query: return jsonify({'results': []}) - + try: from searx.search import SearchWithPlugins from searx.search.models import SearchQuery from searx.query import RawTextQuery from searx.webadapter import get_engineref_from_category_list - + preferences = getattr(request, 'preferences', None) disabled_engines = preferences.engines.get_disabled() if preferences else [] rtq = RawTextQuery(query, disabled_engines) @@ -1434,7 +1468,7 @@ class SXNGPlugin(Plugin): category_list = [c.strip() for c in categories.split(',') if c.strip()] else: category_list = categories or ['general'] - + enginerefs = get_engineref_from_category_list(category_list, disabled_engines) sq = SearchQuery( query=rtq.getQuery(), @@ -1444,19 +1478,19 @@ class SXNGPlugin(Plugin): ) search_obj = SearchWithPlugins(sq, request, user_plugins=[]) result_container = search_obj.search() - + raw_results = result_container.get_ordered_results() raw_infoboxes = getattr(result_container, 'infoboxes', []) raw_answers = getattr(result_container, '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) return jsonify({ 'context': context_str, 'new_urls': new_urls, - 'results': results, + 'results': results, 'infoboxes': infoboxes, 'answers': answers, 'query': query @@ -1876,12 +1910,12 @@ class SXNGPlugin(Plugin): """Builds context string from normalized search data. Returns (context_str, urls).""" context_parts = [] result_urls = [] - + knowledge_graph_lines = [] for ib in infoboxes: ib_name = ib.get('name', '') or ib.get('infobox', '') or ib.get('title', '') ib_content = str(ib.get('content', '')).replace('\n', ' ').strip() - + if ib_name: parts = [f"INFOBOX [{ib_name}]:"] if ib_content: @@ -1891,16 +1925,16 @@ class SXNGPlugin(Plugin): attr_value = attr.get('value', '') if attr_label and attr_value: parts.append(f" {attr_label}: {attr_value}") - + knowledge_graph_lines.append(" ".join(parts) if len(parts) == 2 else "\n".join(parts)) for ans_text in answers: if ans_text and not str(ans_text).startswith('<'): knowledge_graph_lines.append(f"ANSWER: {str(ans_text)[:300]}") - + if knowledge_graph_lines: context_parts.append("KNOWLEDGE GRAPH:\n" + "\n".join(knowledge_graph_lines)) - + deep_lines = [] for i, r in enumerate(clean_results[:self.context_deep_count]): url = r.get('url', '') @@ -1916,10 +1950,10 @@ class SXNGPlugin(Plugin): logger.debug(f"{PLUGIN_NAME}: falling back to snippet for [{idx}] {domain}") content = str(r.get('content', '')).replace('\n', ' ').strip()[:800] deep_lines.append(f"[{idx}] {domain}{date_str}: {title}: {content}") - + if deep_lines: context_parts.append("DEEP SOURCES:\n" + "\n".join(deep_lines)) - + if self.context_shallow_count > 0: shallow_lines = [] start_idx = self.context_deep_count @@ -1931,10 +1965,10 @@ class SXNGPlugin(Plugin): title = r.get('title', '').replace('\n', ' ').strip()[:60] idx = i + 1 + start_idx + offset shallow_lines.append(f"[{idx}] {domain}: {title}") - + if shallow_lines: context_parts.append("SHALLOW SOURCES (headlines):\n" + "\n".join(shallow_lines)) - + return "\n\n".join(context_parts), result_urls 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_infoboxes = getattr(search.result_container, 'infoboxes', []) raw_answers = getattr(search.result_container, 'answers', []) - + q_clean = search.search_query.query.strip() clean_results, infoboxes, answers = self._parse_aux_results(raw_results, raw_infoboxes, raw_answers) clean_results = self._enrich_results(clean_results, q_clean) @@ -1981,12 +2015,12 @@ class SXNGPlugin(Plugin): detected_intent = _detect_intent(q_clean) js_intent = safe_json(detected_intent) - + b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8') total_context_count = self.context_deep_count + self.context_shallow_count - + raw_urls = [r.get('url', '') for r in clean_results[:total_context_count]] - + js_q = safe_json(q_clean) js_lang = safe_json(lang) js_urls = safe_json(raw_urls) @@ -2028,7 +2062,7 @@ class SXNGPlugin(Plugin): .replace("__JS_Q__", js_q) html_payload = f''' -