Compare commits
1 Commits
testing
...
177fd5862e
| Author | SHA1 | Date | |
|---|---|---|---|
| 177fd5862e |
@@ -3,6 +3,7 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
venv/
|
venv/
|
||||||
.env
|
.env
|
||||||
|
dev/.env
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.agent/
|
.agent/
|
||||||
@@ -104,14 +104,44 @@ Configure via environment variables.
|
|||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- [ ] When asking a follow up question the previous output disappears
|
- [ ] Update README with all updates
|
||||||
- [ ] Parts of the UI are not theme aware resulting in a unpolished look when not using a dark theme
|
|
||||||
- [ ] When SearXNG provides a info blob for a search it appears on top of the overview i.e. `Wikipedia` or `Linux`
|
- [x] When asking a follow up question the previous output disappears
|
||||||
|
|
||||||
|
- [x] Parts of the UI are not theme aware resulting in a unpolished look when not using a dark theme
|
||||||
|
|
||||||
|
- [x] When SearXNG provides a info blob for a search it appears on top of the overview i.e. `Wikipedia` or `Linux`
|
||||||
|
|
||||||
For any issues not stated here please create an issue ticket on [Gitea](https://git.tysstech.com/TySS-Dev/ollama-ai-answers-searxng/issues) or [GitHub](https://github.com/TySP-Dev/ollama-ai-answers-searxng/issues) and add the `bug` tag.
|
For any issues not stated here please create an issue ticket on [Gitea](https://git.tysstech.com/TySS-Dev/ollama-ai-answers-searxng/issues) or [GitHub](https://github.com/TySP-Dev/ollama-ai-answers-searxng/issues) and add the `bug` tag.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
### Dev Server
|
||||||
|
|
||||||
|
- [x] Stream viewer — show tokens arriving in real time in the debug panel as they come out of Valkey, so you can see exactly what the LLM is generating chunk by chunk
|
||||||
|
|
||||||
|
- [x] TF-IDF score visualizer — show a table of which URLs were fetched, their scores, and which chunks were selected for context
|
||||||
|
|
||||||
|
- [ ] Intent detection display — show what intent was detected and which system prompt was used for each query
|
||||||
|
|
||||||
|
- [ ] Saved queries — save/load test queries so you can quickly re-run the same set of searches after making changes to the plugin
|
||||||
|
|
||||||
|
- [ ] A/B model comparison — run the same query against two different models simultaneously and show both responses side by side
|
||||||
|
|
||||||
|
- [ ] Response time breakdown — show how long each phase took: SearXNG fetch, page fetching, TF-IDF scoring, LLM stream start, stream complete
|
||||||
|
|
||||||
|
- [ ] Context inspector — show the full assembled context string that gets sent to the LLM, so you can see exactly what it's working with
|
||||||
|
|
||||||
|
- [ ] Prompt viewer — show the full system prompt + user prompt that gets sent to Ollama
|
||||||
|
|
||||||
|
- [ ] Export button — copy the full context + prompt + response as a JSON blob for bug reports
|
||||||
|
|
||||||
|
- [ ] Per-intent system prompt editor — edit the system prompts for each intent type live without restarting
|
||||||
|
|
||||||
|
- [ ] Token counter — show estimated token count of the context being sent
|
||||||
|
|
||||||
|
### Plugin
|
||||||
|
|
||||||
- [ ] Working on feature plans
|
- [ ] Working on feature plans
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|||||||
+180
-103
@@ -1,12 +1,11 @@
|
|||||||
import json, os, logging, base64, time, hashlib, re, http.client, ssl, concurrent.futures, threading, math
|
import json, os, logging, base64, typing, time, hashlib, re, http.client, ssl, concurrent.futures, threading, math
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from searx import network
|
|
||||||
try:
|
try:
|
||||||
from searx.network import get_network
|
from searx.network import get_network
|
||||||
except ImportError:
|
except ImportError:
|
||||||
get_network = None
|
get_network = None
|
||||||
from flask import Response, request, abort, jsonify
|
from flask import request, abort, jsonify
|
||||||
from searx.plugins import Plugin, PluginInfo
|
from searx.plugins import Plugin, PluginInfo
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from searx import settings
|
from searx import settings
|
||||||
@@ -24,7 +23,6 @@ except ImportError:
|
|||||||
logger.warning("AI Answers: valkey package not found. Streaming via Valkey unavailable.")
|
logger.warning("AI Answers: valkey package not found. Streaming via Valkey unavailable.")
|
||||||
|
|
||||||
TOKEN_EXPIRY_SEC = 3600
|
TOKEN_EXPIRY_SEC = 3600
|
||||||
STREAM_CHUNK_SIZE = 512
|
|
||||||
STREAM_TIMEOUT_SEC = 60
|
STREAM_TIMEOUT_SEC = 60
|
||||||
CONV_TTL = 1800
|
CONV_TTL = 1800
|
||||||
|
|
||||||
@@ -276,17 +274,17 @@ INTERACTIVE_CSS = '''
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: 1px solid var(--color-result-border, rgba(0,0,0,0.1));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--color-sidebar-bg, #424247);
|
background: var(--color-base-background-hover, rgba(0,0,0,0.06));
|
||||||
color: var(--color-search-url, #bbb);
|
color: var(--color-base-font, inherit);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.sxng-btn:hover {
|
.sxng-btn:hover {
|
||||||
background: var(--color-search-url, #303033);
|
background: var(--color-result-border, rgba(0,0,0,0.15));
|
||||||
color: var(--color-sidebar-bg, #bbb);
|
color: var(--color-base-font, inherit);
|
||||||
}
|
}
|
||||||
.sxng-btn svg { width: 18px; height: 18px; fill: currentColor; }
|
.sxng-btn svg { width: 18px; height: 18px; fill: currentColor; }
|
||||||
.sxng-input-wrapper {
|
.sxng-input-wrapper {
|
||||||
@@ -300,9 +298,9 @@ INTERACTIVE_CSS = '''
|
|||||||
.sxng-input {
|
.sxng-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: -webkit-fill-available;
|
height: -webkit-fill-available;
|
||||||
background: var(--color-sidebar-bg, #424247);
|
background: var(--color-base-background-hover, rgba(0,0,0,0.06));
|
||||||
border: none;
|
border: 1px solid var(--color-result-border, rgba(0,0,0,0.15));
|
||||||
color: var(--color-base-font, #cdd6f4);
|
color: var(--color-base-font, inherit);
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.78em;
|
font-size: 0.78em;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
@@ -311,7 +309,7 @@ INTERACTIVE_CSS = '''
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.sxng-input:focus { outline: none; }
|
.sxng-input:focus { outline: none; }
|
||||||
.sxng-input::placeholder { color: var(--color-base-font, #333); opacity: 0.35; }
|
.sxng-input::placeholder { color: var(--color-base-font, inherit); opacity: 0.4; }
|
||||||
.sxng-input-line {
|
.sxng-input-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -335,23 +333,24 @@ 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 {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-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: 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;
|
background-color: var(--color-base-background-hover, rgba(0,0,0,0.06));
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
border-width: 0 2rem 0 0;
|
border: 0px solid var(--color-result-border, rgba(0,0,0,0.1));
|
||||||
border-color: transparent;
|
border-right-width: 2rem;
|
||||||
|
border-right-color: transparent;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
color: var(--color-search-url, #bbb);
|
color: var(--color-base-font, inherit);
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
padding: 1px 10px 1px 10px !important;
|
padding: 1px 10px 1px 10px !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -361,8 +360,7 @@ INTERACTIVE_CSS = '''
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.sxng-model-select:hover {
|
.sxng-model-select:hover {
|
||||||
background-color: #303033;
|
background-color: var(--color-result-border, rgba(0,0,0,0.15));
|
||||||
color: var(--color-search-url, #bbb);
|
|
||||||
}
|
}
|
||||||
.sxng-reasoning {
|
.sxng-reasoning {
|
||||||
margin: 0.5rem 0; padding: 0.5rem;
|
margin: 0.5rem 0; padding: 0.5rem;
|
||||||
@@ -385,7 +383,8 @@ INTERACTIVE_CSS = '''
|
|||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
color: var(--color-result-link, #5e81ac);
|
color: var(--color-result-link, #5e81ac);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 0.75;
|
opacity: 1;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.sxng-citation-item a:hover {
|
.sxng-citation-item a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -395,18 +394,18 @@ INTERACTIVE_CSS = '''
|
|||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-left: 2px solid var(--color-result-link, #5e81ac);
|
border-left: 2px solid var(--color-result-link, #5e81ac);
|
||||||
opacity: 0.6;
|
opacity: 0.85;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
.sxng-prior-history summary {
|
.sxng-prior-history summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-result-link, #5e81ac);
|
color: var(--color-result-link, #5e81ac);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.sxng-prior-answer {
|
.sxng-prior-answer {
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
color: var(--color-base-font, #cdd6f4);
|
color: var(--color-base-font, inherit);
|
||||||
}
|
}
|
||||||
.sxng-md-content {
|
.sxng-md-content {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -507,7 +506,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 +541,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';
|
||||||
@@ -600,23 +599,6 @@ INTERACTIVE_JS = r'''
|
|||||||
_ms.appendChild(_o);
|
_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.
|
// conversation saved as base64 URL fragment.
|
||||||
const updateState = () => {
|
const updateState = () => {
|
||||||
try {
|
try {
|
||||||
@@ -636,13 +618,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 +640,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') {
|
||||||
@@ -686,7 +668,6 @@ INTERACTIVE_JS = r'''
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
box.style.display = 'block';
|
box.style.display = 'block';
|
||||||
if(wrapper) wrapper.style.display = '';
|
|
||||||
if(footer && is_interactive) footer.style.display = 'flex';
|
if(footer && is_interactive) footer.style.display = 'flex';
|
||||||
restored = true;
|
restored = true;
|
||||||
}
|
}
|
||||||
@@ -756,10 +737,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 +763,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 +780,7 @@ INTERACTIVE_JS = r'''
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
||||||
await startStream(val, currentText, auxContext);
|
await startStream(val, currentText, auxContext);
|
||||||
updateState();
|
updateState();
|
||||||
} else {
|
} else {
|
||||||
@@ -871,16 +852,92 @@ FRONTEND_JS_TEMPLATE = r"""
|
|||||||
const conversation = {
|
const conversation = {
|
||||||
originalQuery: q_init,
|
originalQuery: q_init,
|
||||||
originalContext: new TextDecoder().decode(Uint8Array.from(atob(b64_init), c => c.charCodeAt(0))),
|
originalContext: new TextDecoder().decode(Uint8Array.from(atob(b64_init), c => c.charCodeAt(0))),
|
||||||
originalSources: [...urls],
|
|
||||||
turns: [{role: 'user', content: q_init, ts: Date.now()}]
|
turns: [{role: 'user', content: q_init, ts: Date.now()}]
|
||||||
};
|
};
|
||||||
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';
|
(function applyTheme() {
|
||||||
|
try {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const s = getComputedStyle(root);
|
||||||
|
const get = (v, fallback) => s.getPropertyValue(v).trim() || fallback;
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
'--color-answer-background': get('--color-answer-background', '#313338'),
|
||||||
|
'--color-answer-font': get('--color-answer-font', '#fff'),
|
||||||
|
'--color-result-link': get('--color-result-link', '#8aacf7'),
|
||||||
|
'--color-base-font': get('--color-base-font', '#cdd6f4'),
|
||||||
|
'--color-sidebar-bg': get('--color-sidebar-bg', '#424247'),
|
||||||
|
'--color-result-hover': get('--color-result-hover', '#303033'),
|
||||||
|
'--color-base-background': get('--color-base-background', '#2a2a2e'),
|
||||||
|
'--color-search-font': get('--color-search-font', '#bbb'),
|
||||||
|
'--color-result-border': get('--color-result-border', '#4c566a'),
|
||||||
|
'--color-result-description':get('--color-result-description', '#d8dee9'),
|
||||||
|
'--color-toolkit-select-background': get('--color-toolkit-select-background', '#313338'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply to box and any ai-answers container
|
||||||
|
const targets = [box, document.getElementById('ai-answers')].filter(Boolean);
|
||||||
|
targets.forEach(el => {
|
||||||
|
Object.entries(theme).forEach(([k, v]) => {
|
||||||
|
if (v) el.style.setProperty(k, v);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
const rootStyle = getComputedStyle(document.documentElement);
|
||||||
|
const getVar = (v, fb) => rootStyle.getPropertyValue(v).trim() || fb;
|
||||||
|
const bg = getVar('--color-answer-background', '');
|
||||||
|
const answerFont = getVar('--color-answer-font', '');
|
||||||
|
// Detect light mode by checking if answer font is dark
|
||||||
|
const isLight = answerFont && (answerFont.includes('0,0,0') ||
|
||||||
|
answerFont.includes('#000') || answerFont.includes('#333') ||
|
||||||
|
answerFont.includes('#444') || answerFont.includes('rgb(0') ||
|
||||||
|
answerFont.includes('rgb(3') || answerFont.includes('rgb(4') ||
|
||||||
|
answerFont.includes('rgb(5') || answerFont.includes('rgb(6'));
|
||||||
|
const containerBg = isLight
|
||||||
|
? 'rgba(0,0,0,0.06)'
|
||||||
|
: (bg || 'var(--color-answer-background, #313338)');
|
||||||
|
aiContainer.style.cssText = [
|
||||||
|
`background: ${containerBg}`,
|
||||||
|
'padding: 1rem',
|
||||||
|
'margin: 0 0 1rem 0',
|
||||||
|
`color: ${getVar('--color-answer-font', 'var(--color-answer-font, #fff)')}`,
|
||||||
|
'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 +1000,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 +1058,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 +1070,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 +1085,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 +1101,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 +1110,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 +1118,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 +1180,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 +1209,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();
|
||||||
@@ -1288,8 +1346,6 @@ INTENT_CONFIGS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
import typing
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from searx.search import SearchWithPlugins
|
from searx.search import SearchWithPlugins
|
||||||
from searx.extended_types import SXNG_Request
|
from searx.extended_types import SXNG_Request
|
||||||
@@ -1379,7 +1435,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 +1445,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 +1453,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 +1476,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 +1490,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 +1500,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
|
||||||
@@ -1669,6 +1725,16 @@ class SXNGPlugin(Plugin):
|
|||||||
|
|
||||||
job_id = hashlib.sha256(f"{time.time()}{q}".encode()).hexdigest()[:16]
|
job_id = hashlib.sha256(f"{time.time()}{q}".encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
# Persist intent for dev UI
|
||||||
|
logger.warning(f"INTENT BEFORE PERSIST: {repr(intent)}")
|
||||||
|
logger.warning(f"JOB_ID BEFORE PERSIST: {repr(job_id)}")
|
||||||
|
try:
|
||||||
|
vk = _get_valkey()
|
||||||
|
vk.setex(f"ai:job:{job_id}:intent", 3600, intent)
|
||||||
|
logger.debug(f"{PLUGIN_NAME}: persisted intent '{intent}' for job {job_id}")
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"{PLUGIN_NAME}: failed to persist intent")
|
||||||
|
|
||||||
payload_dict = {
|
payload_dict = {
|
||||||
"model": effective_model,
|
"model": effective_model,
|
||||||
"messages": [
|
"messages": [
|
||||||
@@ -1876,12 +1942,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 +1957,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 +1982,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 +1997,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 +2024,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 +2047,23 @@ 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)
|
||||||
|
|
||||||
|
# Persist intent for dev tooling / UI
|
||||||
|
try:
|
||||||
|
vk = _get_valkey()
|
||||||
|
vk.setex(
|
||||||
|
f"ai:job:{job_id}:intent",
|
||||||
|
1800,
|
||||||
|
detected_intent
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"{PLUGIN_NAME}: failed to persist intent: {e}")
|
||||||
|
|
||||||
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 +2105,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; }}
|
||||||
@@ -2078,11 +2155,11 @@ class SXNGPlugin(Plugin):
|
|||||||
</style>
|
</style>
|
||||||
<div class="sxng-ai-header">
|
<div class="sxng-ai-header">
|
||||||
<span class="sxng-ai-label">
|
<span class="sxng-ai-label">
|
||||||
<span style="color:#4a9eff;font-size:1.1em;">✦</span> AI Overview
|
<span style="color:var(--color-result-link, #4a9eff);font-size:1.1em;">✦</span> AI Overview
|
||||||
</span>
|
</span>
|
||||||
<select id="sxng-model-select" class="sxng-model-select" title="Select model"></select>
|
<select id="sxng-model-select" class="sxng-model-select" title="Select model"></select>
|
||||||
</div>
|
</div>
|
||||||
<p id="sxng-stream-data" style="white-space: pre-wrap; color: var(--color-result-description); font-size: 0.95rem; margin:0;"><span class="sxng-cursor"></span></p>
|
<p id="sxng-stream-data" style="white-space: pre-wrap; color: var(--color-answer-font, var(--color-result-description, inherit)); font-size: 0.95rem; margin:0;"><span class="sxng-cursor"></span></p>
|
||||||
{interactive_html}
|
{interactive_html}
|
||||||
<script>
|
<script>
|
||||||
{js_code}
|
{js_code}
|
||||||
@@ -2092,4 +2169,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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
flask-babel
|
flask-babel
|
||||||
certifi
|
certifi
|
||||||
|
python-dotenv
|
||||||
|
|||||||
Reference in New Issue
Block a user