This commit is contained in:
@@ -255,6 +255,21 @@
|
|||||||
- backend debug broadcasts
|
- backend debug broadcasts
|
||||||
|
|
||||||
## Recent Change Log
|
## 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`
|
- Date: `2026-03-16`
|
||||||
- What changed:
|
- What changed:
|
||||||
- Hardened search result enrichment and recommendation metadata for preview recovery work.
|
- 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 resultModalGooglePanel = document.getElementById("resultModalGooglePanel");
|
||||||
const resultModalGoogleImage = document.getElementById("resultModalGoogleImage");
|
const resultModalGoogleImage = document.getElementById("resultModalGoogleImage");
|
||||||
const resultModalGoogleText = document.getElementById("resultModalGoogleText");
|
const resultModalGoogleText = document.getElementById("resultModalGoogleText");
|
||||||
|
const resultModalFallbackLabel = document.getElementById("resultModalFallbackLabel");
|
||||||
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
||||||
const resultModalDownload = document.getElementById("resultModalDownload");
|
const resultModalDownload = document.getElementById("resultModalDownload");
|
||||||
const closeResultModal = document.getElementById("closeResultModal");
|
const closeResultModal = document.getElementById("closeResultModal");
|
||||||
@@ -65,6 +66,7 @@ const resultModalReady = Boolean(
|
|||||||
resultModalGooglePanel &&
|
resultModalGooglePanel &&
|
||||||
resultModalGoogleImage &&
|
resultModalGoogleImage &&
|
||||||
resultModalGoogleText &&
|
resultModalGoogleText &&
|
||||||
|
resultModalFallbackLabel &&
|
||||||
resultModalOpenExternal &&
|
resultModalOpenExternal &&
|
||||||
resultModalDownload &&
|
resultModalDownload &&
|
||||||
closeResultModal,
|
closeResultModal,
|
||||||
@@ -88,6 +90,41 @@ function proxiedPreviewURL(src) {
|
|||||||
return `/api/preview/stream?url=${encodeURIComponent(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) {
|
function setStatus(label, progress) {
|
||||||
statusLabel.textContent = label;
|
statusLabel.textContent = label;
|
||||||
statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
||||||
@@ -393,13 +430,19 @@ function hideModal(element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildResultModalEmbedURL(item) {
|
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) {
|
if (!item?.link) {
|
||||||
return "about:blank";
|
return "about:blank";
|
||||||
}
|
}
|
||||||
if (item.source === "Google Video") {
|
if (item.source === "Google Video") {
|
||||||
const videoId = extractYouTubeID(item.link);
|
const videoId = extractYouTubeID(item.link);
|
||||||
if (videoId) {
|
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;
|
return item.link;
|
||||||
@@ -409,12 +452,14 @@ function resetResultModalMedia() {
|
|||||||
if (!resultModalReady) {
|
if (!resultModalReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
resultModalFrame.onload = null;
|
||||||
resultModalFrame.src = "about:blank";
|
resultModalFrame.src = "about:blank";
|
||||||
resultModalVideo.pause();
|
resultModalVideo.pause();
|
||||||
detachVideoSource(resultModalVideo);
|
detachVideoSource(resultModalVideo);
|
||||||
resultModalThumbnail.removeAttribute("src");
|
resultModalThumbnail.removeAttribute("src");
|
||||||
resultModalGoogleImage.removeAttribute("src");
|
resultModalGoogleImage.removeAttribute("src");
|
||||||
resultModalGoogleText.textContent = "";
|
resultModalGoogleText.textContent = "";
|
||||||
|
resultModalFallbackLabel.textContent = "Preview Fallback";
|
||||||
resultModalMediaFrame.style.aspectRatio = "";
|
resultModalMediaFrame.style.aspectRatio = "";
|
||||||
setHidden(resultModalFrame, true, "");
|
setHidden(resultModalFrame, true, "");
|
||||||
setHidden(resultModalVideo, true, "");
|
setHidden(resultModalVideo, true, "");
|
||||||
@@ -444,13 +489,27 @@ function showResultModalThumbnail(src, alt) {
|
|||||||
setHidden(resultModalThumbnail, false, "");
|
setHidden(resultModalThumbnail, false, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showResultModalGooglePanel(item) {
|
function showResultModalGooglePanel(item, message = "") {
|
||||||
resultModalGoogleImage.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER;
|
resultModalFallbackLabel.textContent = item.source || "Preview Fallback";
|
||||||
|
resultModalGoogleImage.src = hasUsableThumbnail(item.thumbnailUrl) ? item.thumbnailUrl : PREVIEW_PLACEHOLDER;
|
||||||
resultModalGoogleImage.alt = item.title || "";
|
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");
|
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) {
|
function renderResults(results) {
|
||||||
searchResults.innerHTML = "";
|
searchResults.innerHTML = "";
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
@@ -461,15 +520,28 @@ function renderResults(results) {
|
|||||||
const node = cardTemplate.content.firstElementChild.cloneNode(true);
|
const node = cardTemplate.content.firstElementChild.cloneNode(true);
|
||||||
const image = node.querySelector("img");
|
const image = node.querySelector("img");
|
||||||
const previewVideo = node.querySelector(".preview-hover");
|
const previewVideo = node.querySelector(".preview-hover");
|
||||||
|
const mediaFallback = node.querySelector(".media-fallback");
|
||||||
const overlays = node.querySelectorAll(".preview-overlay");
|
const overlays = node.querySelectorAll(".preview-overlay");
|
||||||
image.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER;
|
const usableThumbnail = hasUsableThumbnail(item.thumbnailUrl);
|
||||||
image.alt = item.title;
|
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("h3").textContent = item.title;
|
||||||
node.querySelector(".result-snippet").textContent = item.reason || item.snippet || item.source || "";
|
node.querySelector(".result-snippet").textContent = summarizeReason(item.reason) || item.snippet || item.source || "";
|
||||||
node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : "";
|
node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : (item.previewBlockedReason || "");
|
||||||
node.querySelector(".source-badge").textContent = item.source;
|
node.querySelector(".source-badge").textContent = item.source;
|
||||||
node.addEventListener("click", () => openResultModal(item));
|
node.addEventListener("click", () => openResultModal(item));
|
||||||
previewVideo.poster = item.thumbnailUrl || "";
|
previewVideo.poster = usableThumbnail ? item.thumbnailUrl : "";
|
||||||
if (item.previewVideoUrl) {
|
if (item.previewVideoUrl) {
|
||||||
const mediaArea = node.querySelector(".relative");
|
const mediaArea = node.querySelector(".relative");
|
||||||
mediaArea.addEventListener("mouseenter", () => {
|
mediaArea.addEventListener("mouseenter", () => {
|
||||||
@@ -521,16 +593,32 @@ function openResultModal(item) {
|
|||||||
activeResultItem = item;
|
activeResultItem = item;
|
||||||
resultModalTitle.textContent = item.title || "Untitled";
|
resultModalTitle.textContent = item.title || "Untitled";
|
||||||
resultModalSource.textContent = item.source || "";
|
resultModalSource.textContent = item.source || "";
|
||||||
resultModalReason.textContent = item.reason || "AI 노트가 없습니다.";
|
resultModalReason.textContent = summarizeReason(item.reason) || "AI 노트가 없습니다.";
|
||||||
resultModalSnippet.textContent = item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.";
|
resultModalSnippet.textContent = item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.";
|
||||||
resultModalOpenExternal.href = item.link || "#";
|
resultModalOpenExternal.href = item.link || "#";
|
||||||
const canDirectDownload = item.source === "Google Video" && item.link;
|
const canDirectDownload = item.source === "Google Video" && item.link;
|
||||||
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
||||||
resetResultModalMedia();
|
resetResultModalMedia();
|
||||||
if (item.source === "Google Video") {
|
const embedURL = buildResultModalEmbedURL(item);
|
||||||
showResultModalGooglePanel(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 {
|
} else {
|
||||||
showResultModalFrame(item.link || "about:blank");
|
fallbackResultModalMedia(item, fallbackReason);
|
||||||
}
|
}
|
||||||
showModal(resultModal);
|
showModal(resultModal);
|
||||||
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
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="" />
|
<img id="resultModalGoogleImage" class="aspect-video w-full object-cover" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3 rounded-2xl border border-white/10 bg-white/[0.04] p-5 text-left">
|
<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>
|
<p id="resultModalGoogleText" class="text-sm leading-7 text-zinc-200"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,6 +202,9 @@
|
|||||||
<div class="relative aspect-video overflow-hidden bg-zinc-900">
|
<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="" />
|
<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>
|
<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="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 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>
|
</div>
|
||||||
@@ -213,6 +216,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/app.js?v=20260316g" defer></script>
|
<script src="/app.js?v=20260316h" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user