mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
Show thumbnails on past research cards
This commit is contained in:
+1
-1
@@ -117,7 +117,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
|
f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
|
||||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||||
"font-src 'self' https://cdn.jsdelivr.net; "
|
"font-src 'self' https://cdn.jsdelivr.net; "
|
||||||
"img-src 'self' data: blob:; "
|
"img-src 'self' data: blob: https:; "
|
||||||
"media-src 'self' blob:; "
|
"media-src 'self' blob:; "
|
||||||
"connect-src 'self'; "
|
"connect-src 'self'; "
|
||||||
"frame-src 'self'; "
|
"frame-src 'self'; "
|
||||||
|
|||||||
@@ -31,6 +31,54 @@ _NON_CHAT_MODEL = (
|
|||||||
"moderation", "rerank", "reranker", "clip", "stable-diffusion",
|
"moderation", "rerank", "reranker", "clip", "stable-diffusion",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_RESEARCH_IMAGE_BLOCKLIST = {
|
||||||
|
"cdn.shopify.com/s/files/1/0179/4388/7926/files/icon.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_research_icon_or_logo_url(url: str) -> bool:
|
||||||
|
path = url.lower().split("?")[0]
|
||||||
|
return any(token in path for token in (
|
||||||
|
"/logo", "logo_", "-logo", "favicon", "apple-touch-icon",
|
||||||
|
"sprite", "icon-", "_icon", "/icons/", "badge",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def _research_thumbnail(data: dict) -> str:
|
||||||
|
"""Pick the same first visible image the visual report uses as hero."""
|
||||||
|
hidden = set(data.get("hidden_images") or [])
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def usable(image: str) -> bool:
|
||||||
|
image = str(image or "").strip()
|
||||||
|
if not image or image in seen or image in hidden:
|
||||||
|
return False
|
||||||
|
if not image.startswith("https://"):
|
||||||
|
return False
|
||||||
|
if image.endswith((".svg", ".ico", ".gif")):
|
||||||
|
return False
|
||||||
|
if any(blocked in image for blocked in _RESEARCH_IMAGE_BLOCKLIST):
|
||||||
|
return False
|
||||||
|
if _is_research_icon_or_logo_url(image):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
for source in data.get("sources") or []:
|
||||||
|
if not isinstance(source, dict):
|
||||||
|
continue
|
||||||
|
image = str(source.get("image") or source.get("og_image") or "").strip()
|
||||||
|
if usable(image):
|
||||||
|
seen.add(image)
|
||||||
|
return image
|
||||||
|
for finding in data.get("raw_findings") or data.get("findings") or []:
|
||||||
|
if not isinstance(finding, dict):
|
||||||
|
continue
|
||||||
|
image = str(finding.get("image") or finding.get("og_image") or "").strip()
|
||||||
|
if usable(image):
|
||||||
|
seen.add(image)
|
||||||
|
return image
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _first_chat_model(models) -> str:
|
def _first_chat_model(models) -> str:
|
||||||
"""First model that isn't an embedding/tts/etc. — falls back to models[0]."""
|
"""First model that isn't an embedding/tts/etc. — falls back to models[0]."""
|
||||||
@@ -290,6 +338,7 @@ def setup_research_routes(research_handler, session_manager=None) -> APIRouter:
|
|||||||
"started_at": d.get("started_at", 0),
|
"started_at": d.get("started_at", 0),
|
||||||
"completed_at": d.get("completed_at", 0),
|
"completed_at": d.get("completed_at", 0),
|
||||||
"archived": bool(d.get("archived")),
|
"archived": bool(d.get("archived")),
|
||||||
|
"thumbnail": _research_thumbnail(d),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ import themeModule from './js/theme.js';
|
|||||||
// unversioned so this can't recur.
|
// unversioned so this can't recur.
|
||||||
import cookbookModule from './js/cookbook.js';
|
import cookbookModule from './js/cookbook.js';
|
||||||
import groupModule from './js/group.js';
|
import groupModule from './js/group.js';
|
||||||
import * as researchPanelModule from './js/research/panel.js';
|
import * as researchPanelModule from './js/research/panel.js?v=20260630researchthumb';
|
||||||
import ttsModule from './js/tts-ai.js';
|
import ttsModule from './js/tts-ai.js';
|
||||||
import spinnerModule from './js/spinner.js';
|
import spinnerModule from './js/spinner.js';
|
||||||
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
|
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
|
||||||
|
|||||||
+3
-3
@@ -219,8 +219,8 @@
|
|||||||
}, { once: true });
|
}, { once: true });
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260630toolmetrics">
|
<link rel="stylesheet" href="/static/style.css?v=20260630researchthumb">
|
||||||
<link rel="modulepreload" href="/static/app.js">
|
<link rel="modulepreload" href="/static/app.js?v=20260630researchthumb">
|
||||||
<link rel="modulepreload" href="/static/js/chat.js">
|
<link rel="modulepreload" href="/static/js/chat.js">
|
||||||
<link rel="modulepreload" href="/static/js/ui.js">
|
<link rel="modulepreload" href="/static/js/ui.js">
|
||||||
<link rel="modulepreload" href="/static/js/sessions.js">
|
<link rel="modulepreload" href="/static/js/sessions.js">
|
||||||
@@ -2512,7 +2512,7 @@
|
|||||||
<script type="module" src="/static/js/settings.js"></script>
|
<script type="module" src="/static/js/settings.js"></script>
|
||||||
<script type="module" src="/static/js/admin.js"></script>
|
<script type="module" src="/static/js/admin.js"></script>
|
||||||
<script type="module" src="/static/js/assistant.js"></script>
|
<script type="module" src="/static/js/assistant.js"></script>
|
||||||
<script type="module" src="/static/app.js"></script> <!-- app.js must be LAST -->
|
<script type="module" src="/static/app.js?v=20260630researchthumb"></script> <!-- app.js must be LAST -->
|
||||||
<script type="module" src="/static/js/init.js"></script>
|
<script type="module" src="/static/js/init.js"></script>
|
||||||
<script type="module" src="/static/js/a11y.js"></script>
|
<script type="module" src="/static/js/a11y.js"></script>
|
||||||
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script>
|
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script>
|
||||||
|
|||||||
@@ -99,13 +99,34 @@ async function _syncLibrary(options = {}) {
|
|||||||
for (const item of (libData.research || [])) {
|
for (const item of (libData.research || [])) {
|
||||||
if (item.status !== 'done') continue;
|
if (item.status !== 'done') continue;
|
||||||
if (dismissed.has(item.id)) continue;
|
if (dismissed.has(item.id)) continue;
|
||||||
if (_jobs.some(j => j.id === item.id)) continue;
|
|
||||||
const elapsed = item.duration ? _parseDuration(item.duration) : 0;
|
const elapsed = item.duration ? _parseDuration(item.duration) : 0;
|
||||||
|
const existing = _jobs.find(j => j.id === item.id);
|
||||||
|
if (existing) {
|
||||||
|
let changed = false;
|
||||||
|
const updates = {
|
||||||
|
query: item.query || existing.query,
|
||||||
|
status: 'done',
|
||||||
|
elapsed: elapsed || existing.elapsed || 0,
|
||||||
|
sourceCount: item.source_count || existing.sourceCount || 0,
|
||||||
|
thumbnail: item.thumbnail || existing.thumbnail || '',
|
||||||
|
category: item.category || existing.category || '',
|
||||||
|
_fromLibrary: true,
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
if (existing[key] !== value) {
|
||||||
|
existing[key] = value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) _notify();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
_jobs.push({
|
_jobs.push({
|
||||||
id: item.id, query: item.query, status: 'done',
|
id: item.id, query: item.query, status: 'done',
|
||||||
progress: {}, startedAt: (item.started_at || 0) * 1000,
|
progress: {}, startedAt: (item.started_at || 0) * 1000,
|
||||||
elapsed, result: null, sources: null, findings: null,
|
elapsed, result: null, sources: null, findings: null,
|
||||||
sourceCount: item.source_count || 0,
|
sourceCount: item.source_count || 0,
|
||||||
|
thumbnail: item.thumbnail || '',
|
||||||
category: item.category || '',
|
category: item.category || '',
|
||||||
errorMsg: null, avgDuration: null, modelName: null,
|
errorMsg: null, avgDuration: null, modelName: null,
|
||||||
settings: { max_rounds: item.rounds || 8 },
|
settings: { max_rounds: item.rounds || 8 },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Deep Research side panel — open/close, form, job rendering, library.
|
* Deep Research side panel — open/close, form, job rendering, library.
|
||||||
*/
|
*/
|
||||||
import * as jobs from './jobs.js';
|
import * as jobs from './jobs.js?v=20260630researchthumb';
|
||||||
import themeModule from '../theme.js';
|
import themeModule from '../theme.js';
|
||||||
import createResearchSynapse from '../researchSynapse.js';
|
import createResearchSynapse from '../researchSynapse.js';
|
||||||
import spinnerModule from '../spinner.js';
|
import spinnerModule from '../spinner.js';
|
||||||
@@ -994,6 +994,11 @@ function _buildJobCard(job) {
|
|||||||
const failNote = failed
|
const failNote = failed
|
||||||
? `<div class="research-job-failnote">Couldn't extract anything — try rephrasing the question, or switch the search engine in Settings.</div>`
|
? `<div class="research-job-failnote">Couldn't extract anything — try rephrasing the question, or switch the search engine in Settings.</div>`
|
||||||
: '';
|
: '';
|
||||||
|
const thumbSource = (job.sources || []).find(s => s && (s.image || s.og_image));
|
||||||
|
const thumbUrl = job.thumbnail || thumbSource?.image || thumbSource?.og_image || '';
|
||||||
|
const thumbnail = thumbUrl
|
||||||
|
? `<img class="research-job-thumb" src="${_esc(thumbUrl)}" alt="" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
|
: '<span class="research-job-thumb research-job-thumb-empty" aria-hidden="true"></span>';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="research-job-header">
|
<div class="research-job-header">
|
||||||
<span class="research-job-query">${_esc(job.query)}</span>${doneBadge}
|
<span class="research-job-query">${_esc(job.query)}</span>${doneBadge}
|
||||||
@@ -1002,6 +1007,7 @@ function _buildJobCard(job) {
|
|||||||
</div>
|
</div>
|
||||||
${failNote}
|
${failNote}
|
||||||
<div class="research-job-actions">
|
<div class="research-job-actions">
|
||||||
|
${thumbnail}
|
||||||
<button class="research-job-action research-job-action-report" data-action="report" title="Visual report">${_externalIcon} Visual Report</button>
|
<button class="research-job-action research-job-action-report" data-action="report" title="Visual report">${_externalIcon} Visual Report</button>
|
||||||
<button class="research-job-action" data-action="chat" title="Open follow-up chat with this research as context">${_chatIcon} Discuss</button>
|
<button class="research-job-action" data-action="chat" title="Open follow-up chat with this research as context">${_chatIcon} Discuss</button>
|
||||||
<button class="research-job-action research-job-action-dim" data-action="copy" title="Copy report to clipboard">${_copyIcon}</button>
|
<button class="research-job-action research-job-action-dim" data-action="copy" title="Copy report to clipboard">${_copyIcon}</button>
|
||||||
|
|||||||
@@ -38270,6 +38270,23 @@ body.research-panel-view #research-divider { display:none; }
|
|||||||
}
|
}
|
||||||
.research-job-actions {
|
.research-job-actions {
|
||||||
display:flex; gap:4px; margin-top:6px;
|
display:flex; gap:4px; margin-top:6px;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
.research-job-thumb {
|
||||||
|
width: 64px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex: 0 0 64px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||||
|
background: var(--panel, var(--bg));
|
||||||
|
}
|
||||||
|
.research-job-thumb-empty {
|
||||||
|
display: inline-block;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg,
|
||||||
|
color-mix(in srgb, var(--fg) 7%, transparent),
|
||||||
|
color-mix(in srgb, var(--fg) 3%, transparent));
|
||||||
}
|
}
|
||||||
/* Push the first dim (dismiss/delete) button — and everything after it — to
|
/* Push the first dim (dismiss/delete) button — and everything after it — to
|
||||||
the right edge of the actions row. `:first-of-type` would match the very
|
the right edge of the actions row. `:first-of-type` would match the very
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
|
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
|
||||||
// - API / non-GET: never cached.
|
// - API / non-GET: never cached.
|
||||||
// Bump CACHE_NAME whenever the precache list or SW logic changes.
|
// Bump CACHE_NAME whenever the precache list or SW logic changes.
|
||||||
const CACHE_NAME = 'odysseus-v327';
|
const CACHE_NAME = 'odysseus-v328';
|
||||||
|
|
||||||
// Core shell precached on install so repeat opens are instant without any
|
// Core shell precached on install so repeat opens are instant without any
|
||||||
// network wait. Keep this list in sync with the <script type="module"> tags
|
// network wait. Keep this list in sync with the <script type="module"> tags
|
||||||
|
|||||||
Reference in New Issue
Block a user