This commit is contained in:
@@ -255,6 +255,21 @@
|
||||
- backend debug broadcasts
|
||||
|
||||
## Recent Change Log
|
||||
- Date: `2026-03-16`
|
||||
- What changed:
|
||||
- Rewired the result modal to consume backend media metadata instead of hard-coded source branches.
|
||||
- Google Video now uses embed-first modal rendering again, with iframe timeout fallback to thumbnail/panel mode.
|
||||
- Search cards now suppress low-value favicon/logo thumbnails and show a neutral “preview unavailable” media state instead of tiny site icons or placeholder-like junk.
|
||||
- Bumped frontend asset version so browsers pick up the new modal logic.
|
||||
- Why it changed:
|
||||
- The UI was still rendering Google Video as a static image panel, and Envato/Artgrid cards were surfacing unusable thumbnails that made the results look broken even when metadata existed.
|
||||
- How it was verified:
|
||||
- `go test ./...`
|
||||
- `bash scripts/selftest.sh`
|
||||
- What is still risky or incomplete:
|
||||
- Browser-level validation was still not fully reproducible here, so iframe timeout behavior and provider-specific rendering quirks still need live confirmation in the deployed UI.
|
||||
- `node` is not installed in this environment, so frontend syntax/build verification is still limited to static inspection plus app boot smoke testing.
|
||||
|
||||
- Date: `2026-03-16`
|
||||
- What changed:
|
||||
- Hardened search result enrichment and recommendation metadata for preview recovery work.
|
||||
|
||||
+101
-13
@@ -49,6 +49,7 @@ const resultModalThumbnail = document.getElementById("resultModalThumbnail");
|
||||
const resultModalGooglePanel = document.getElementById("resultModalGooglePanel");
|
||||
const resultModalGoogleImage = document.getElementById("resultModalGoogleImage");
|
||||
const resultModalGoogleText = document.getElementById("resultModalGoogleText");
|
||||
const resultModalFallbackLabel = document.getElementById("resultModalFallbackLabel");
|
||||
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
||||
const resultModalDownload = document.getElementById("resultModalDownload");
|
||||
const closeResultModal = document.getElementById("closeResultModal");
|
||||
@@ -65,6 +66,7 @@ const resultModalReady = Boolean(
|
||||
resultModalGooglePanel &&
|
||||
resultModalGoogleImage &&
|
||||
resultModalGoogleText &&
|
||||
resultModalFallbackLabel &&
|
||||
resultModalOpenExternal &&
|
||||
resultModalDownload &&
|
||||
closeResultModal,
|
||||
@@ -88,6 +90,41 @@ function proxiedPreviewURL(src) {
|
||||
return `/api/preview/stream?url=${encodeURIComponent(src)}`;
|
||||
}
|
||||
|
||||
function isLowValueThumbnailURL(src) {
|
||||
const lower = String(src || "").toLowerCase();
|
||||
if (!lower) {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
"favicon",
|
||||
"apple-touch-icon",
|
||||
"/logo",
|
||||
"/icon",
|
||||
"icon.",
|
||||
"logo.",
|
||||
"placehold.co",
|
||||
].some((token) => lower.includes(token)) ||
|
||||
((lower.includes("googleusercontent.com") || lower.includes("gstatic.com") || lower.includes("bing.com") || lower.includes("duckduckgo.com")) && !lower.includes("ytimg.com"));
|
||||
}
|
||||
|
||||
function hasUsableThumbnail(src) {
|
||||
return Boolean(src) && !isLowValueThumbnailURL(src);
|
||||
}
|
||||
|
||||
function summarizeReason(reason) {
|
||||
const text = String(reason || "").trim();
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
if (text === "Ranked candidate pending stronger visual evidence.") {
|
||||
return "Preview evidence pending";
|
||||
}
|
||||
if (text === "Fallback due to missing provider preview.") {
|
||||
return "Provider preview missing";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function setStatus(label, progress) {
|
||||
statusLabel.textContent = label;
|
||||
statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
||||
@@ -393,13 +430,19 @@ function hideModal(element) {
|
||||
}
|
||||
|
||||
function buildResultModalEmbedURL(item) {
|
||||
if (item?.embedUrl) {
|
||||
if (item.source === "Google Video" && item.embedUrl.includes("youtube-nocookie.com/embed/")) {
|
||||
return `${item.embedUrl}&origin=${encodeURIComponent(window.location.origin)}`;
|
||||
}
|
||||
return item.embedUrl;
|
||||
}
|
||||
if (!item?.link) {
|
||||
return "about:blank";
|
||||
}
|
||||
if (item.source === "Google Video") {
|
||||
const videoId = extractYouTubeID(item.link);
|
||||
if (videoId) {
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&origin=${encodeURIComponent(window.location.origin)}`;
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&modestbranding=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`;
|
||||
}
|
||||
}
|
||||
return item.link;
|
||||
@@ -409,12 +452,14 @@ function resetResultModalMedia() {
|
||||
if (!resultModalReady) {
|
||||
return;
|
||||
}
|
||||
resultModalFrame.onload = null;
|
||||
resultModalFrame.src = "about:blank";
|
||||
resultModalVideo.pause();
|
||||
detachVideoSource(resultModalVideo);
|
||||
resultModalThumbnail.removeAttribute("src");
|
||||
resultModalGoogleImage.removeAttribute("src");
|
||||
resultModalGoogleText.textContent = "";
|
||||
resultModalFallbackLabel.textContent = "Preview Fallback";
|
||||
resultModalMediaFrame.style.aspectRatio = "";
|
||||
setHidden(resultModalFrame, true, "");
|
||||
setHidden(resultModalVideo, true, "");
|
||||
@@ -444,13 +489,27 @@ function showResultModalThumbnail(src, alt) {
|
||||
setHidden(resultModalThumbnail, false, "");
|
||||
}
|
||||
|
||||
function showResultModalGooglePanel(item) {
|
||||
resultModalGoogleImage.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER;
|
||||
function showResultModalGooglePanel(item, message = "") {
|
||||
resultModalFallbackLabel.textContent = item.source || "Preview Fallback";
|
||||
resultModalGoogleImage.src = hasUsableThumbnail(item.thumbnailUrl) ? item.thumbnailUrl : PREVIEW_PLACEHOLDER;
|
||||
resultModalGoogleImage.alt = item.title || "";
|
||||
resultModalGoogleText.textContent = item.snippet || item.reason || "YouTube 페이지 열기 또는 Direct Download를 사용할 수 있습니다.";
|
||||
resultModalGoogleText.textContent = message || item.previewBlockedReason || item.snippet || item.reason || "Preview fallback is being shown.";
|
||||
setHidden(resultModalGooglePanel, false, "flex");
|
||||
}
|
||||
|
||||
function fallbackResultModalMedia(item, reason) {
|
||||
logEvent("result:modal:fallback", { title: item.title, source: item.source, reason, mediaMode: item.mediaMode });
|
||||
if (item.previewVideoUrl) {
|
||||
showResultModalVideo(item.previewVideoUrl);
|
||||
return;
|
||||
}
|
||||
if (hasUsableThumbnail(item.thumbnailUrl)) {
|
||||
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
|
||||
return;
|
||||
}
|
||||
showResultModalGooglePanel(item, reason || "Preview fallback is being shown.");
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
searchResults.innerHTML = "";
|
||||
if (!results.length) {
|
||||
@@ -461,15 +520,28 @@ function renderResults(results) {
|
||||
const node = cardTemplate.content.firstElementChild.cloneNode(true);
|
||||
const image = node.querySelector("img");
|
||||
const previewVideo = node.querySelector(".preview-hover");
|
||||
const mediaFallback = node.querySelector(".media-fallback");
|
||||
const overlays = node.querySelectorAll(".preview-overlay");
|
||||
image.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER;
|
||||
image.alt = item.title;
|
||||
const usableThumbnail = hasUsableThumbnail(item.thumbnailUrl);
|
||||
if (usableThumbnail) {
|
||||
image.src = item.thumbnailUrl;
|
||||
image.alt = item.title;
|
||||
image.classList.remove("hidden");
|
||||
mediaFallback.classList.add("hidden");
|
||||
} else {
|
||||
image.removeAttribute("src");
|
||||
image.alt = "";
|
||||
image.classList.add("hidden");
|
||||
mediaFallback.classList.remove("hidden");
|
||||
mediaFallback.classList.add("flex");
|
||||
mediaFallback.textContent = item.source === "Envato" || item.source === "Artgrid" ? `${item.source} preview unavailable` : "Preview unavailable";
|
||||
}
|
||||
node.querySelector("h3").textContent = item.title;
|
||||
node.querySelector(".result-snippet").textContent = item.reason || item.snippet || item.source || "";
|
||||
node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : "";
|
||||
node.querySelector(".result-snippet").textContent = summarizeReason(item.reason) || item.snippet || item.source || "";
|
||||
node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : (item.previewBlockedReason || "");
|
||||
node.querySelector(".source-badge").textContent = item.source;
|
||||
node.addEventListener("click", () => openResultModal(item));
|
||||
previewVideo.poster = item.thumbnailUrl || "";
|
||||
previewVideo.poster = usableThumbnail ? item.thumbnailUrl : "";
|
||||
if (item.previewVideoUrl) {
|
||||
const mediaArea = node.querySelector(".relative");
|
||||
mediaArea.addEventListener("mouseenter", () => {
|
||||
@@ -521,16 +593,32 @@ function openResultModal(item) {
|
||||
activeResultItem = item;
|
||||
resultModalTitle.textContent = item.title || "Untitled";
|
||||
resultModalSource.textContent = item.source || "";
|
||||
resultModalReason.textContent = item.reason || "AI 노트가 없습니다.";
|
||||
resultModalReason.textContent = summarizeReason(item.reason) || "AI 노트가 없습니다.";
|
||||
resultModalSnippet.textContent = item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.";
|
||||
resultModalOpenExternal.href = item.link || "#";
|
||||
const canDirectDownload = item.source === "Google Video" && item.link;
|
||||
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
||||
resetResultModalMedia();
|
||||
if (item.source === "Google Video") {
|
||||
showResultModalGooglePanel(item);
|
||||
const embedURL = buildResultModalEmbedURL(item);
|
||||
const fallbackReason = item.previewBlockedReason || "Embedded view was unavailable, switched to fallback preview.";
|
||||
if (item.mediaMode === "embed" && embedURL && embedURL !== "about:blank") {
|
||||
showResultModalFrame(embedURL);
|
||||
const timeout = window.setTimeout(() => {
|
||||
logEvent("result:modal:iframe_timeout", { title: item.title, source: item.source, embedURL });
|
||||
if (activeResultItem?.link === item.link) {
|
||||
resetResultModalMedia();
|
||||
fallbackResultModalMedia(item, fallbackReason);
|
||||
}
|
||||
}, item.source === "Google Video" ? 5000 : 2000);
|
||||
resultModalFrame.onload = () => {
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
} else if (item.mediaMode === "preview_video" && item.previewVideoUrl) {
|
||||
showResultModalVideo(item.previewVideoUrl);
|
||||
} else if (item.mediaMode === "thumbnail" && hasUsableThumbnail(item.thumbnailUrl)) {
|
||||
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
|
||||
} else {
|
||||
showResultModalFrame(item.link || "about:blank");
|
||||
fallbackResultModalMedia(item, fallbackReason);
|
||||
}
|
||||
showModal(resultModal);
|
||||
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
||||
|
||||
+5
-2
@@ -172,7 +172,7 @@
|
||||
<img id="resultModalGoogleImage" class="aspect-video w-full object-cover" alt="" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 rounded-2xl border border-white/10 bg-white/[0.04] p-5 text-left">
|
||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Google Video</p>
|
||||
<p id="resultModalFallbackLabel" class="text-xs uppercase tracking-[0.25em] text-zinc-500">Preview Fallback</p>
|
||||
<p id="resultModalGoogleText" class="text-sm leading-7 text-zinc-200"></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,6 +202,9 @@
|
||||
<div class="relative aspect-video overflow-hidden bg-zinc-900">
|
||||
<img class="h-full w-full object-cover transition duration-500 group-hover:scale-105" alt="" />
|
||||
<video class="preview-hover absolute inset-0 hidden h-full w-full object-cover" muted loop playsinline preload="none"></video>
|
||||
<div class="media-fallback absolute inset-0 hidden items-center justify-center bg-[radial-gradient(circle_at_top,#2b3342,transparent_60%),linear-gradient(180deg,#111827,#05070b)] p-5 text-center text-xs uppercase tracking-[0.24em] text-zinc-300">
|
||||
Preview unavailable
|
||||
</div>
|
||||
<div class="preview-overlay absolute left-3 top-3 rounded-full border border-white/20 bg-black/60 px-3 py-1 text-[11px] uppercase tracking-[0.25em] text-white">AI Recommended</div>
|
||||
<div class="source-badge preview-overlay absolute bottom-3 left-3 rounded-full bg-white px-3 py-1 text-[11px] font-medium uppercase tracking-[0.2em] text-black"></div>
|
||||
</div>
|
||||
@@ -213,6 +216,6 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script src="/app.js?v=20260316g" defer></script>
|
||||
<script src="/app.js?v=20260316h" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user