This commit is contained in:
@@ -255,6 +255,22 @@
|
|||||||
- backend debug broadcasts
|
- backend debug broadcasts
|
||||||
|
|
||||||
## Recent Change Log
|
## Recent Change Log
|
||||||
|
- Date: `2026-03-16`
|
||||||
|
- What changed:
|
||||||
|
- Result modal layout was rebuilt to match a top `16:9` embedded viewer with bottom-left full AI note and bottom-right action panel.
|
||||||
|
- Google Video results now load YouTube embed URLs in the modal viewer and keep the white `Direct Download` action in the lower-right panel.
|
||||||
|
- When Gemini evaluation comes back mostly negative or too weak, the backend now runs one supplemental search pass with broader intent variants and reevaluates the merged pool.
|
||||||
|
- Failed Gemini batch evaluations now retry sequentially candidate-by-candidate with a short delay so more candidates can still be processed when batch/token evaluation is unstable.
|
||||||
|
- Why it changed:
|
||||||
|
- The requested modal information hierarchy was different from the previous implementation.
|
||||||
|
- The user wanted negative Gemini feedback to trigger more exploration instead of stopping at the first pool.
|
||||||
|
- Batch-level Gemini failures were causing too many results to skip evaluation entirely.
|
||||||
|
- How it was verified:
|
||||||
|
- code-path inspection against the updated modal wiring and search flow
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- Non-YouTube third-party pages can still refuse iframe embedding via CSP or `X-Frame-Options`.
|
||||||
|
- Sequential Gemini retries improve coverage but also increase latency when the model is degraded.
|
||||||
|
|
||||||
- Date: `2026-03-16`
|
- Date: `2026-03-16`
|
||||||
- What changed:
|
- What changed:
|
||||||
- Bumped frontend asset version and added result-modal initialization guards to avoid click failures when browser cache serves mismatched HTML/JS.
|
- Bumped frontend asset version and added result-modal initialization guards to avoid click failures when browser cache serves mismatched HTML/JS.
|
||||||
|
|||||||
@@ -323,6 +323,18 @@ func (a *App) searchMedia(c *gin.Context) {
|
|||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
||||||
recommended, geminiStats, geminiErr := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
recommended, geminiStats, geminiErr := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||||
a.debug("search gemini evaluation", geminiStats)
|
a.debug("search gemini evaluation", geminiStats)
|
||||||
|
if services.NeedsSupplementalExploration(recommended) {
|
||||||
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini 평가가 약해 추가 후보를 탐색하는 중", "progress": 82})
|
||||||
|
explorationQueries := buildSupplementalQueries(req.Query, queryVariants)
|
||||||
|
extraResults, extraErr := a.SearchService.SearchMedia(explorationQueries, enabledPlatforms)
|
||||||
|
if extraErr == nil && len(extraResults) > 0 {
|
||||||
|
results = mergeSearchResults(results, extraResults)
|
||||||
|
scored = services.RankSearchResults(strings.Join(explorationQueries[:min(len(explorationQueries), 3)], " "), results)
|
||||||
|
recommended, geminiStats, geminiErr = services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||||
|
a.debug("search supplemental query variants", gin.H{"variants": explorationQueries, "variantCount": len(explorationQueries)})
|
||||||
|
a.debug("search gemini evaluation after supplemental search", geminiStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
if geminiErr != nil && len(recommended) == 0 {
|
if geminiErr != nil && len(recommended) == 0 {
|
||||||
warning := geminiErr.Error()
|
warning := geminiErr.Error()
|
||||||
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
||||||
@@ -419,6 +431,45 @@ func selectedPlatformLabel(platforms map[string]bool) string {
|
|||||||
return strings.Join(labels, ", ")
|
return strings.Join(labels, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildSupplementalQueries(query string, existing []string) []string {
|
||||||
|
candidates := append([]string{}, existing...)
|
||||||
|
candidates = append(candidates,
|
||||||
|
query+" cinematic stock footage",
|
||||||
|
query+" editorial b-roll",
|
||||||
|
query+" establishing shot",
|
||||||
|
query+" drone footage",
|
||||||
|
)
|
||||||
|
|
||||||
|
seen := map[string]bool{}
|
||||||
|
result := make([]string, 0, len(candidates))
|
||||||
|
for _, item := range candidates {
|
||||||
|
trimmed := strings.Join(strings.Fields(strings.TrimSpace(item)), " ")
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(trimmed)
|
||||||
|
if seen[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeSearchResults(base, extra []services.SearchResult) []services.SearchResult {
|
||||||
|
merged := make([]services.SearchResult, 0, len(base)+len(extra))
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, item := range append(base, extra...) {
|
||||||
|
if item.Link == "" || seen[item.Link] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item.Link] = true
|
||||||
|
merged = append(merged, item)
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
func summarizeSearchResults(results []services.SearchResult, duration time.Duration, geminiCap int, warning string) searchDebugSummary {
|
func summarizeSearchResults(results []services.SearchResult, duration time.Duration, geminiCap int, warning string) searchDebugSummary {
|
||||||
bySource := map[string]int{}
|
bySource := map[string]int{}
|
||||||
withPreview := 0
|
withPreview := 0
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import (
|
|||||||
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
|
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
|
||||||
|
|
||||||
type GeminiBatchStats struct {
|
type GeminiBatchStats struct {
|
||||||
CandidateCap int `json:"candidateCap"`
|
CandidateCap int `json:"candidateCap"`
|
||||||
Requested int `json:"requested"`
|
Requested int `json:"requested"`
|
||||||
Batches int `json:"batches"`
|
Batches int `json:"batches"`
|
||||||
Succeeded int `json:"succeeded"`
|
Succeeded int `json:"succeeded"`
|
||||||
Failed int `json:"failed"`
|
Failed int `json:"failed"`
|
||||||
RecommendedCount int `json:"recommendedCount"`
|
SequentialRetried int `json:"sequentialRetried"`
|
||||||
Errors []string `json:"errors,omitempty"`
|
RecommendedCount int `json:"recommendedCount"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RankSearchResults(query string, results []SearchResult) []SearchResult {
|
func RankSearchResults(query string, results []SearchResult) []SearchResult {
|
||||||
@@ -142,6 +143,27 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
|||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, batch := range results {
|
for _, batch := range results {
|
||||||
if batch.err != nil {
|
if batch.err != nil {
|
||||||
|
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize)
|
||||||
|
if len(recovered) > 0 {
|
||||||
|
stats.SequentialRetried++
|
||||||
|
stats.Succeeded++
|
||||||
|
for _, item := range recovered {
|
||||||
|
if item.Link == "" || seen[item.Link] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item.Link] = true
|
||||||
|
merged = append(merged, item)
|
||||||
|
}
|
||||||
|
if len(recoveredErrs) > 0 {
|
||||||
|
stats.Failed++
|
||||||
|
for _, recoveredErr := range recoveredErrs {
|
||||||
|
if len(stats.Errors) < 5 {
|
||||||
|
stats.Errors = append(stats.Errors, recoveredErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
stats.Failed++
|
stats.Failed++
|
||||||
if len(stats.Errors) < 5 {
|
if len(stats.Errors) < 5 {
|
||||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
stats.Errors = append(stats.Errors, batch.err.Error())
|
||||||
@@ -210,6 +232,62 @@ func RandomizeTopRecommendations(items []AIRecommendation, window int) []AIRecom
|
|||||||
return shuffled
|
return shuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex int) ([]AIRecommendation, []string) {
|
||||||
|
recovered := make([]AIRecommendation, 0, 8)
|
||||||
|
errs := make([]string, 0, 4)
|
||||||
|
endIndex := min(startIndex+8, len(ranked))
|
||||||
|
for idx := startIndex; idx < endIndex; idx++ {
|
||||||
|
recs, err := service.Recommend(query, []SearchResult{ranked[idx]})
|
||||||
|
if err != nil {
|
||||||
|
if len(errs) < 4 {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
}
|
||||||
|
time.Sleep(350 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recovered = append(recovered, recs...)
|
||||||
|
time.Sleep(350 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return recovered, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendedCount := 0
|
||||||
|
negativeCount := 0
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Recommended {
|
||||||
|
recommendedCount++
|
||||||
|
}
|
||||||
|
if looksNegativeReason(item.Reason) {
|
||||||
|
negativeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if recommendedCount >= 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return negativeCount >= max(2, len(items)/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksNegativeReason(reason string) bool {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(reason))
|
||||||
|
if lower == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range []string{
|
||||||
|
"부적합", "관련이 없", "맞지 않", "의도와 맞지", "무관", "연관성 낮", "적절하지 않", "불일치",
|
||||||
|
"not relevant", "irrelevant", "mismatch", "does not match", "unsuitable",
|
||||||
|
} {
|
||||||
|
if strings.Contains(lower, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
|
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
|
||||||
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
|
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
@@ -251,3 +329,10 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
|||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|||||||
+18
-37
@@ -42,26 +42,20 @@ const resultModalTitle = document.getElementById("resultModalTitle");
|
|||||||
const resultModalSource = document.getElementById("resultModalSource");
|
const resultModalSource = document.getElementById("resultModalSource");
|
||||||
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
||||||
const resultModalReason = document.getElementById("resultModalReason");
|
const resultModalReason = document.getElementById("resultModalReason");
|
||||||
|
const resultModalFrame = document.getElementById("resultModalFrame");
|
||||||
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");
|
||||||
const resultModalMediaFrame = document.getElementById("resultModalMediaFrame");
|
|
||||||
const resultModalVideo = document.getElementById("resultModalVideo");
|
|
||||||
const resultModalThumbnail = document.getElementById("resultModalThumbnail");
|
|
||||||
const resultModalEmbedNotice = document.getElementById("resultModalEmbedNotice");
|
|
||||||
const resultModalReady = Boolean(
|
const resultModalReady = Boolean(
|
||||||
resultModal &&
|
resultModal &&
|
||||||
resultModalTitle &&
|
resultModalTitle &&
|
||||||
resultModalSource &&
|
resultModalSource &&
|
||||||
resultModalSnippet &&
|
resultModalSnippet &&
|
||||||
resultModalReason &&
|
resultModalReason &&
|
||||||
|
resultModalFrame &&
|
||||||
resultModalOpenExternal &&
|
resultModalOpenExternal &&
|
||||||
resultModalDownload &&
|
resultModalDownload &&
|
||||||
closeResultModal &&
|
closeResultModal,
|
||||||
resultModalMediaFrame &&
|
|
||||||
resultModalVideo &&
|
|
||||||
resultModalThumbnail &&
|
|
||||||
resultModalEmbedNotice,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let pendingDownload = null;
|
let pendingDownload = null;
|
||||||
@@ -362,16 +356,24 @@ function hideModal(element) {
|
|||||||
setHidden(element, true);
|
setHidden(element, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildResultModalEmbedURL(item) {
|
||||||
|
if (!item?.link) {
|
||||||
|
return "about:blank";
|
||||||
|
}
|
||||||
|
if (item.source === "Google Video") {
|
||||||
|
const videoId = extractYouTubeID(item.link);
|
||||||
|
if (videoId) {
|
||||||
|
return `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item.link;
|
||||||
|
}
|
||||||
|
|
||||||
function resetResultModalMedia() {
|
function resetResultModalMedia() {
|
||||||
if (!resultModalReady) {
|
if (!resultModalReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resultModalVideo.pause();
|
resultModalFrame.src = "about:blank";
|
||||||
detachVideoSource(resultModalVideo);
|
|
||||||
resultModalMediaFrame.style.aspectRatio = "";
|
|
||||||
setHidden(resultModalVideo, true, "");
|
|
||||||
setHidden(resultModalThumbnail, true, "");
|
|
||||||
setHidden(resultModalEmbedNotice, false, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
@@ -450,16 +452,7 @@ function openResultModal(item) {
|
|||||||
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.previewVideoUrl) {
|
resultModalFrame.src = buildResultModalEmbedURL(item);
|
||||||
attachVideoSource(resultModalVideo, item.previewVideoUrl);
|
|
||||||
setHidden(resultModalVideo, false, "");
|
|
||||||
setHidden(resultModalEmbedNotice, true, "");
|
|
||||||
} else if (item.thumbnailUrl) {
|
|
||||||
resultModalThumbnail.src = item.thumbnailUrl;
|
|
||||||
resultModalThumbnail.alt = item.title || "";
|
|
||||||
setHidden(resultModalThumbnail, false, "");
|
|
||||||
setHidden(resultModalEmbedNotice, true, "");
|
|
||||||
}
|
|
||||||
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 });
|
||||||
}
|
}
|
||||||
@@ -690,18 +683,6 @@ previewThumbnail.addEventListener("load", () => {
|
|||||||
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (resultModalReady) {
|
|
||||||
resultModalVideo.addEventListener("loadedmetadata", () => {
|
|
||||||
if (resultModalVideo.videoWidth > 0 && resultModalVideo.videoHeight > 0) {
|
|
||||||
resultModalMediaFrame.style.aspectRatio = `${resultModalVideo.videoWidth} / ${resultModalVideo.videoHeight}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resultModalThumbnail.addEventListener("load", () => {
|
|
||||||
if (!resultModalVideo.src && resultModalThumbnail.naturalWidth > 0 && resultModalThumbnail.naturalHeight > 0) {
|
|
||||||
resultModalMediaFrame.style.aspectRatio = `${resultModalThumbnail.naturalWidth} / ${resultModalThumbnail.naturalHeight}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (const button of platformToggles) {
|
for (const button of platformToggles) {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const platform = button.dataset.platformToggle;
|
const platform = button.dataset.platformToggle;
|
||||||
|
|||||||
+18
-24
@@ -150,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="resultModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 px-4">
|
<div id="resultModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 px-4">
|
||||||
<div class="flex h-[88vh] w-full max-w-7xl flex-col overflow-hidden rounded-3xl border border-white/10 bg-zinc-950 shadow-2xl">
|
<div class="flex w-full max-w-7xl flex-col overflow-hidden rounded-3xl border border-white/10 bg-zinc-950 shadow-2xl">
|
||||||
<div class="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
<div class="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p id="resultModalSource" class="text-xs uppercase tracking-[0.25em] text-zinc-500"></p>
|
<p id="resultModalSource" class="text-xs uppercase tracking-[0.25em] text-zinc-500"></p>
|
||||||
@@ -161,29 +161,23 @@
|
|||||||
<button id="closeResultModal" type="button" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Close</button>
|
<button id="closeResultModal" type="button" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[1.5fr_0.85fr]">
|
<div class="border-b border-white/10 bg-black/40 p-4">
|
||||||
<div class="min-h-0 border-b border-white/10 lg:border-b-0 lg:border-r">
|
<div class="aspect-video overflow-hidden rounded-2xl border border-white/10 bg-black">
|
||||||
<div id="resultModalMediaFrame" class="flex h-full min-h-[320px] items-center justify-center overflow-hidden bg-black/40 p-4">
|
<iframe id="resultModalFrame" class="h-full w-full bg-white" referrerpolicy="no-referrer" allow="autoplay; fullscreen; encrypted-media; picture-in-picture" allowfullscreen></iframe>
|
||||||
<video id="resultModalVideo" class="hidden max-h-full w-full rounded-2xl bg-black object-contain" controls playsinline></video>
|
|
||||||
<img id="resultModalThumbnail" class="hidden max-h-full w-full rounded-2xl object-contain" alt="" />
|
|
||||||
<div id="resultModalEmbedNotice" class="max-w-md rounded-3xl border border-white/10 bg-white/[0.03] p-6 text-center text-sm leading-7 text-zinc-300">
|
|
||||||
외부 사이트 임베딩 차단 문제를 피하기 위해 내부 미리보기만 표시합니다.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 overflow-auto px-5 py-5">
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="grid gap-5 px-5 py-5 lg:grid-cols-[1.6fr_0.8fr]">
|
||||||
<div>
|
<div class="rounded-2xl border border-white/10 bg-white/[0.03] p-5">
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
|
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
|
||||||
<p id="resultModalReason" class="mt-2 whitespace-pre-wrap text-sm leading-7 text-zinc-200"></p>
|
<p id="resultModalReason" class="mt-3 whitespace-pre-wrap text-sm leading-7 text-zinc-200"></p>
|
||||||
<button id="resultModalDownload" type="button" class="mt-4 hidden w-full rounded-2xl border border-white bg-white px-4 py-3 text-sm font-medium text-black transition hover:bg-zinc-200">
|
</div>
|
||||||
Direct Download
|
<div class="space-y-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5">
|
||||||
</button>
|
<button id="resultModalDownload" type="button" class="hidden w-full rounded-2xl border border-white bg-white px-4 py-3 text-sm font-medium text-black transition hover:bg-zinc-200">
|
||||||
</div>
|
Direct Download
|
||||||
<div>
|
</button>
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Source Summary</p>
|
<div>
|
||||||
<p id="resultModalSnippet" class="mt-2 text-sm leading-7 text-zinc-300"></p>
|
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Source Summary</p>
|
||||||
</div>
|
<p id="resultModalSnippet" class="mt-3 text-sm leading-7 text-zinc-300"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +200,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/app.js?v=20260316b" defer></script>
|
<script src="/app.js?v=20260316c" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user