Show thumbnails on past research cards

This commit is contained in:
pewdiepie-archdaemon
2026-06-30 05:17:46 +00:00
parent b8338b2399
commit a72ec0c116
8 changed files with 101 additions and 8 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' 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:; "
"connect-src 'self'; "
"frame-src 'self'; "
+49
View File
@@ -31,6 +31,54 @@ _NON_CHAT_MODEL = (
"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:
"""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),
"completed_at": d.get("completed_at", 0),
"archived": bool(d.get("archived")),
"thumbnail": _research_thumbnail(d),
})
except Exception:
continue
+1 -1
View File
@@ -39,7 +39,7 @@ import themeModule from './js/theme.js';
// unversioned so this can't recur.
import cookbookModule from './js/cookbook.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 spinnerModule from './js/spinner.js';
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
+3 -3
View File
@@ -219,8 +219,8 @@
}, { once: true });
})();
</script>
<link rel="stylesheet" href="/static/style.css?v=20260630toolmetrics">
<link rel="modulepreload" href="/static/app.js">
<link rel="stylesheet" href="/static/style.css?v=20260630researchthumb">
<link rel="modulepreload" href="/static/app.js?v=20260630researchthumb">
<link rel="modulepreload" href="/static/js/chat.js">
<link rel="modulepreload" href="/static/js/ui.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/admin.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/a11y.js"></script>
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script>
+22 -1
View File
@@ -99,13 +99,34 @@ async function _syncLibrary(options = {}) {
for (const item of (libData.research || [])) {
if (item.status !== 'done') 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 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({
id: item.id, query: item.query, status: 'done',
progress: {}, startedAt: (item.started_at || 0) * 1000,
elapsed, result: null, sources: null, findings: null,
sourceCount: item.source_count || 0,
thumbnail: item.thumbnail || '',
category: item.category || '',
errorMsg: null, avgDuration: null, modelName: null,
settings: { max_rounds: item.rounds || 8 },
+7 -1
View File
@@ -1,7 +1,7 @@
/**
* 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 createResearchSynapse from '../researchSynapse.js';
import spinnerModule from '../spinner.js';
@@ -994,6 +994,11 @@ function _buildJobCard(job) {
const failNote = failed
? `<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 = `
<div class="research-job-header">
<span class="research-job-query">${_esc(job.query)}</span>${doneBadge}
@@ -1002,6 +1007,7 @@ function _buildJobCard(job) {
</div>
${failNote}
<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" 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>
+17
View File
@@ -38270,6 +38270,23 @@ body.research-panel-view #research-divider { display:none; }
}
.research-job-actions {
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
the right edge of the actions row. `:first-of-type` would match the very
+1 -1
View File
@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached.
// 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
// network wait. Keep this list in sync with the <script type="module"> tags