Compare commits
7 Commits
932f08642c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 914f10f502 | |||
| e3dbedc59f | |||
| 73d820ddaa | |||
| f5ceb872e0 | |||
| e79d15de2e | |||
| 3c6df2e777 | |||
| 1fb9919ec3 |
@@ -268,6 +268,86 @@
|
|||||||
- backend debug broadcasts
|
- backend debug broadcasts
|
||||||
|
|
||||||
## Recent Change Log
|
## Recent Change Log
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Removed the remaining inline `Powered by GIPHY` / prompt-chip bar from Zone A image mode so the image search view now shows only the shared search controls and the results area.
|
||||||
|
- Why it changed:
|
||||||
|
- The user wanted that image-mode top strip removed entirely instead of reduced or restyled.
|
||||||
|
- How it was verified:
|
||||||
|
- static review of `frontend/index.html` and `frontend/app.js`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- None beyond the usual need for a browser hard refresh if an older cached frontend bundle is still open in a tab.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Restored the video-search request path to tolerate a scheme-less `SEARXNG_BASE_URL` such as `192.168.1.66:8087` by normalizing it to `http://...` during search-service initialization.
|
||||||
|
- Added regression coverage so the video search service keeps accepting the older style base URL configuration used in live deployment.
|
||||||
|
- Why it changed:
|
||||||
|
- Real user logs showed video search failing immediately with `first path segment in URL cannot contain colon`, which traced back to a scheme-less SearXNG base URL in the deployed environment.
|
||||||
|
- How it was verified:
|
||||||
|
- log review of `ai-media-hub-2026-03-24T08-09-23-204Z.log`
|
||||||
|
- added unit coverage for scheme-less base URL normalization
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- Go tests could not be rerun in this environment because `go` is currently unavailable here, so this fix is verified by code-path review plus the added test only.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Corrected the Unraid template GIPHY download path mapping from `/downloads/giphy` to `/app/downloads/giphy` so it matches the backend default download directory layout.
|
||||||
|
- Why it changed:
|
||||||
|
- The previous template target path dropped the `/app` prefix and did not match the application’s runtime default for `GIPHY_DOWNLOAD_DIR`.
|
||||||
|
- How it was verified:
|
||||||
|
- static review of `unraid-template.xml`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- Existing Unraid installs that already created the older path mapping may need their template field refreshed or reapplied to align with the corrected container path.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Removed the large GIPHY image-mode info box entirely and replaced it with a minimal inline prompt bar plus `Powered by GIPHY` label.
|
||||||
|
- Hardened frontend visibility toggling so stale cached HTML/JS combinations do not crash on missing elements.
|
||||||
|
- Bumped the frontend asset version again so browsers are forced onto the latest image-search UI bundle after the GIPHY panel changes.
|
||||||
|
- Why it changed:
|
||||||
|
- Real user logs showed a client-side `Cannot read properties of null (reading 'classList')` error caused by stale frontend asset mismatch, which prevented image results from rendering, and the remaining large image-mode box was still not desired in the UI.
|
||||||
|
- How it was verified:
|
||||||
|
- log review of `ai-media-hub-2026-03-24T07-48-19-085Z.log`
|
||||||
|
- `node --check frontend/app.js`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- Users with aggressively cached browser sessions may still need one hard refresh to fully drop older HTML/JS combinations already loaded in an open tab.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Simplified the GIPHY image-search UX so it presents raw search results instead of looking like an AI-evaluated result flow.
|
||||||
|
- Updated the image-mode copy to describe direct GIPHY search results, and changed the shared preview modal labels/content for GIPHY items from AI-note style metadata to plain result/source info.
|
||||||
|
- Why it changed:
|
||||||
|
- The image-search experience should behave like a straightforward provider search result browser, not like the video-side Gemini review flow.
|
||||||
|
- How it was verified:
|
||||||
|
- `node --check frontend/app.js`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- This is a UX clarification pass; the backend still uses Gemini only for multilingual query expansion and does not do visual evaluation on GIPHY items.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Relaxed Gemini image-query expansion parsing so loose plain-text numbered lists can still be accepted when the model prepends explanatory text instead of returning a clean JSON object.
|
||||||
|
- Removed the GIPHY image-mode search meta box from the frontend so the image UI stays visually simpler.
|
||||||
|
- Stopped surfacing the Gemini image-expansion fallback warning directly in the image-search UI when the backend can still continue with usable fallback queries.
|
||||||
|
- Why it changed:
|
||||||
|
- Real log review showed Gemini image expansion sometimes returned text like `Here is the JSON requested`, which triggered fallback even though the model output still contained useful query candidates, and the extra meta box was not adding enough value to justify the space it consumed.
|
||||||
|
- How it was verified:
|
||||||
|
- log review of `ai-media-hub-2026-03-24T07-25-42-827Z.log`
|
||||||
|
- `node --check frontend/app.js`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- This improves tolerance for one common Gemini formatting deviation, but fully free-form model output can still fall back if it does not contain recoverable query lines.
|
||||||
|
- Go tests still could not be rerun in this environment because `go` is currently unavailable here.
|
||||||
|
|
||||||
|
- Date: `2026-03-24`
|
||||||
|
- What changed:
|
||||||
|
- Removed the redundant `GIPHY Download Dir` variable field from the Unraid template and kept the dedicated `GIPHY Downloads` path mapping as the single user-facing download-path control.
|
||||||
|
- Why it changed:
|
||||||
|
- The earlier template exposed both a path mapping and a matching container-path variable for the same GIPHY download location, which was unnecessarily confusing in Unraid.
|
||||||
|
- How it was verified:
|
||||||
|
- static review of `unraid-template.xml`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- The application still supports `GIPHY_DOWNLOAD_DIR` as an environment variable, but the Unraid template now intentionally relies on the path mapping plus the backend default container path to reduce duplicated inputs.
|
||||||
|
|
||||||
- Date: `2026-03-24`
|
- Date: `2026-03-24`
|
||||||
- What changed:
|
- What changed:
|
||||||
- Replaced the earlier frontend-only image prototype with an integrated GIPHY image/GIF search flow.
|
- Replaced the earlier frontend-only image prototype with an integrated GIPHY image/GIF search flow.
|
||||||
|
|||||||
+12
-1
@@ -63,7 +63,7 @@ func NewSearchService(baseURL, googleVideoEngine, webEngine string) *SearchServi
|
|||||||
webEngine = "google"
|
webEngine = "google"
|
||||||
}
|
}
|
||||||
return &SearchService{
|
return &SearchService{
|
||||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
BaseURL: normalizeBaseURL(baseURL),
|
||||||
GoogleVideoEngine: googleVideoEngine,
|
GoogleVideoEngine: googleVideoEngine,
|
||||||
WebEngine: webEngine,
|
WebEngine: webEngine,
|
||||||
Client: &http.Client{Timeout: 20 * time.Second},
|
Client: &http.Client{Timeout: 20 * time.Second},
|
||||||
@@ -77,6 +77,17 @@ func NewSearchService(baseURL, googleVideoEngine, webEngine string) *SearchServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeBaseURL(raw string) string {
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if !strings.Contains(trimmed, "://") {
|
||||||
|
trimmed = "http://" + trimmed
|
||||||
|
}
|
||||||
|
return strings.TrimRight(trimmed, "/")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SearchService) SearchMedia(queries []string, enabledPlatforms map[string]bool) ([]SearchResult, SearchExecutionMeta, error) {
|
func (s *SearchService) SearchMedia(queries []string, enabledPlatforms map[string]bool) ([]SearchResult, SearchExecutionMeta, error) {
|
||||||
return s.SearchMediaWithDeadline(queries, enabledPlatforms, time.Time{})
|
return s.SearchMediaWithDeadline(queries, enabledPlatforms, time.Time{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ func TestSearchServiceFetchCacheRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewSearchServiceNormalizesSchemeLessBaseURL(t *testing.T) {
|
||||||
|
service := NewSearchService("192.168.1.66:8087", "", "")
|
||||||
|
if service.BaseURL != "http://192.168.1.66:8087" {
|
||||||
|
t.Fatalf("expected normalized base url, got %q", service.BaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSplitSearchDeadlinesReservesEnrichmentWindow(t *testing.T) {
|
func TestSplitSearchDeadlinesReservesEnrichmentWindow(t *testing.T) {
|
||||||
deadline := time.Now().Add(20 * time.Second)
|
deadline := time.Now().Add(20 * time.Second)
|
||||||
collectionDeadline, enrichmentDeadline := splitSearchDeadlines(deadline)
|
collectionDeadline, enrichmentDeadline := splitSearchDeadlines(deadline)
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ func (g *GeminiService) ExpandImageQueries(query string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
jsonText, err := extractJSONObject(rawText)
|
jsonText, err := extractJSONObject(rawText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if looseQueries := parseLooseImageExpansionLines(rawText); len(looseQueries) == 5 {
|
||||||
|
g.setCachedExpansion(cacheKey, looseQueries, 15*time.Minute)
|
||||||
|
g.debug("gemini:image_expand_success", map[string]any{"query": trimmed, "queries": looseQueries, "mode": "loose_text"})
|
||||||
|
return looseQueries, nil
|
||||||
|
}
|
||||||
g.debug("gemini:image_expand_parse_error", map[string]any{"query": trimmed, "error": err.Error()})
|
g.debug("gemini:image_expand_parse_error", map[string]any{"query": trimmed, "error": err.Error()})
|
||||||
g.setCachedExpansion(cacheKey, fallback, 15*time.Minute)
|
g.setCachedExpansion(cacheKey, fallback, 15*time.Minute)
|
||||||
return fallback, err
|
return fallback, err
|
||||||
@@ -801,6 +806,34 @@ func truncateForError(text string, limit int) string {
|
|||||||
return trimmed[:limit] + "..."
|
return trimmed[:limit] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseLooseImageExpansionLines(text string) []string {
|
||||||
|
candidates := make([]string, 0, 8)
|
||||||
|
for _, line := range strings.Split(text, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "- ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "* ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "1. ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "2. ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "3. ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "4. ")
|
||||||
|
trimmed = strings.TrimPrefix(trimmed, "5. ")
|
||||||
|
trimmed = strings.TrimSpace(strings.Trim(trimmed, "\"'`"))
|
||||||
|
lower := strings.ToLower(trimmed)
|
||||||
|
if strings.HasPrefix(lower, "here is") || strings.HasPrefix(lower, "json") || strings.HasPrefix(lower, "output") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = append(candidates, trimmed)
|
||||||
|
}
|
||||||
|
queries := normalizeImageExpansionQueries(candidates)
|
||||||
|
if len(queries) < 5 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return queries[:5]
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeKoreanReason(reason string) string {
|
func normalizeKoreanReason(reason string) string {
|
||||||
trimmed := strings.TrimSpace(reason)
|
trimmed := strings.TrimSpace(reason)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
|
|||||||
@@ -145,6 +145,26 @@ func TestExpandImageQueriesFallsBackWhenGeminiFails(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpandImageQueriesAcceptsLoosePlainTextList(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"Here is the JSON requested\n1. cute cat\n2. cute cat gif\n3. cat reaction gif\n4. cat meme gif\n5. animated cat sticker"}]}}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := NewGeminiService("dummy-key", "gemini-2.5-flash")
|
||||||
|
service.Client = &http.Client{Timeout: 2 * time.Second}
|
||||||
|
service.GenerateEndpoint = server.URL
|
||||||
|
|
||||||
|
queries, err := service.ExpandImageQueries("고양이")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected loose plain-text list to be accepted, got %v", err)
|
||||||
|
}
|
||||||
|
if len(queries) != 5 || queries[0] != "cute cat" || queries[4] != "animated cat sticker" {
|
||||||
|
t.Fatalf("unexpected loose parsed queries: %#v", queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelectUnevaluatedCandidatesSkipsReviewedLinks(t *testing.T) {
|
func TestSelectUnevaluatedCandidatesSkipsReviewedLinks(t *testing.T) {
|
||||||
ranked := []SearchResult{
|
ranked := []SearchResult{
|
||||||
{Link: "https://a.example"},
|
{Link: "https://a.example"},
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ func (s *GiphyService) SearchImages(query string, requestedMax int) (GiphySearch
|
|||||||
expandedQueries, expansionErr := s.expandQueries(response.OriginalQuery)
|
expandedQueries, expansionErr := s.expandQueries(response.OriginalQuery)
|
||||||
response.ExpandedQueries = expandedQueries
|
response.ExpandedQueries = expandedQueries
|
||||||
if expansionErr != nil {
|
if expansionErr != nil {
|
||||||
response.Warning = "Query expansion failed, using fallback search terms."
|
|
||||||
s.debug("giphy:query_expansion_fallback", map[string]any{
|
s.debug("giphy:query_expansion_fallback", map[string]any{
|
||||||
"query": response.OriginalQuery,
|
"query": response.OriginalQuery,
|
||||||
"queries": expandedQueries,
|
"queries": expandedQueries,
|
||||||
|
|||||||
+18
-51
@@ -11,12 +11,6 @@ const searchSubmitButton = document.getElementById("searchSubmitButton");
|
|||||||
const searchResultsViewport = document.getElementById("searchResultsViewport");
|
const searchResultsViewport = document.getElementById("searchResultsViewport");
|
||||||
const mediaTypeToggles = Array.from(document.querySelectorAll("[data-media-type-toggle]"));
|
const mediaTypeToggles = Array.from(document.querySelectorAll("[data-media-type-toggle]"));
|
||||||
const platformToggles = Array.from(document.querySelectorAll("[data-platform-toggle]"));
|
const platformToggles = Array.from(document.querySelectorAll("[data-platform-toggle]"));
|
||||||
const imageSearchSandbox = document.getElementById("imageSearchSandbox");
|
|
||||||
const imagePromptChips = Array.from(document.querySelectorAll("[data-image-prompt]"));
|
|
||||||
const giphyMetaPanel = document.getElementById("giphyMetaPanel");
|
|
||||||
const giphyOriginalQuery = document.getElementById("giphyOriginalQuery");
|
|
||||||
const giphyResultCount = document.getElementById("giphyResultCount");
|
|
||||||
const giphyExpandedQueries = document.getElementById("giphyExpandedQueries");
|
|
||||||
const dropzone = document.getElementById("dropzone");
|
const dropzone = document.getElementById("dropzone");
|
||||||
const fileInput = document.getElementById("fileInput");
|
const fileInput = document.getElementById("fileInput");
|
||||||
const uploadResult = document.getElementById("uploadResult");
|
const uploadResult = document.getElementById("uploadResult");
|
||||||
@@ -52,6 +46,8 @@ const debugSummary = document.getElementById("debugSummary");
|
|||||||
const resultModal = document.getElementById("resultModal");
|
const resultModal = document.getElementById("resultModal");
|
||||||
const resultModalTitle = document.getElementById("resultModalTitle");
|
const resultModalTitle = document.getElementById("resultModalTitle");
|
||||||
const resultModalSource = document.getElementById("resultModalSource");
|
const resultModalSource = document.getElementById("resultModalSource");
|
||||||
|
const resultModalReasonLabel = document.getElementById("resultModalReasonLabel");
|
||||||
|
const resultModalSnippetLabel = document.getElementById("resultModalSnippetLabel");
|
||||||
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 resultModalFrame = document.getElementById("resultModalFrame");
|
||||||
@@ -70,6 +66,8 @@ const resultModalReady = Boolean(
|
|||||||
resultModal &&
|
resultModal &&
|
||||||
resultModalTitle &&
|
resultModalTitle &&
|
||||||
resultModalSource &&
|
resultModalSource &&
|
||||||
|
resultModalReasonLabel &&
|
||||||
|
resultModalSnippetLabel &&
|
||||||
resultModalSnippet &&
|
resultModalSnippet &&
|
||||||
resultModalReason &&
|
resultModalReason &&
|
||||||
resultModalFrame &&
|
resultModalFrame &&
|
||||||
@@ -103,7 +101,6 @@ const resultPreviewInflight = new Map();
|
|||||||
let cardSummaryObserver = null;
|
let cardSummaryObserver = null;
|
||||||
let activeMediaType = "video";
|
let activeMediaType = "video";
|
||||||
const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
|
const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
|
||||||
let activeImageSearchResponse = null;
|
|
||||||
|
|
||||||
function proxiedPreviewURL(src) {
|
function proxiedPreviewURL(src) {
|
||||||
if (!src) {
|
if (!src) {
|
||||||
@@ -170,6 +167,9 @@ function setStatus(label, progress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setHidden(element, hidden, visibleDisplayClass = "flex") {
|
function setHidden(element, hidden, visibleDisplayClass = "flex") {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
element.classList.toggle("hidden", hidden);
|
element.classList.toggle("hidden", hidden);
|
||||||
if (visibleDisplayClass) {
|
if (visibleDisplayClass) {
|
||||||
element.classList.toggle(visibleDisplayClass, !hidden);
|
element.classList.toggle(visibleDisplayClass, !hidden);
|
||||||
@@ -310,31 +310,6 @@ function renderImageEmptyState(message) {
|
|||||||
searchResults.innerHTML = `<div class="rounded-3xl border border-white/10 bg-black/30 p-5 text-sm text-zinc-400">${message}</div>`;
|
searchResults.innerHTML = `<div class="rounded-3xl border border-white/10 bg-black/30 p-5 text-sm text-zinc-400">${message}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExpandedQueries(queries = []) {
|
|
||||||
giphyExpandedQueries.innerHTML = "";
|
|
||||||
for (const item of queries) {
|
|
||||||
const chip = document.createElement("span");
|
|
||||||
chip.className = "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-zinc-300";
|
|
||||||
chip.textContent = item;
|
|
||||||
giphyExpandedQueries.appendChild(chip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateImageSearchMeta(data = null) {
|
|
||||||
activeImageSearchResponse = data;
|
|
||||||
const visible = Boolean(data);
|
|
||||||
setHidden(giphyMetaPanel, !visible, "block");
|
|
||||||
if (!visible) {
|
|
||||||
giphyOriginalQuery.textContent = "Original query: -";
|
|
||||||
giphyResultCount.textContent = "0 results";
|
|
||||||
giphyExpandedQueries.innerHTML = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
giphyOriginalQuery.textContent = `Original query: ${data.originalQuery || "-"}`;
|
|
||||||
giphyResultCount.textContent = `${Number(data.total || 0)} results`;
|
|
||||||
renderExpandedQueries(data.expandedQueries || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderImageResults(items = []) {
|
function renderImageResults(items = []) {
|
||||||
searchResults.innerHTML = "";
|
searchResults.innerHTML = "";
|
||||||
searchResults.classList.remove("xl:grid-cols-3");
|
searchResults.classList.remove("xl:grid-cols-3");
|
||||||
@@ -361,14 +336,12 @@ function renderImageResults(items = []) {
|
|||||||
function applyMediaTypeUI() {
|
function applyMediaTypeUI() {
|
||||||
const isImageMode = activeMediaType === "image";
|
const isImageMode = activeMediaType === "image";
|
||||||
syncMediaTypeButtons();
|
syncMediaTypeButtons();
|
||||||
setHidden(imageSearchSandbox, !isImageMode, "block");
|
|
||||||
setHidden(giphyMetaPanel, true, "block");
|
|
||||||
setHidden(queryVariants, true, "");
|
setHidden(queryVariants, true, "");
|
||||||
showWarning("");
|
showWarning("");
|
||||||
searchResultsViewport.classList.toggle("image-results-scroll", isImageMode);
|
searchResultsViewport.classList.toggle("image-results-scroll", isImageMode);
|
||||||
searchModeTitle.textContent = isImageMode ? "AI Image Discovery" : "AI Smart Discovery";
|
searchModeTitle.textContent = isImageMode ? "AI Image Discovery" : "AI Smart Discovery";
|
||||||
searchModeHint.textContent = isImageMode
|
searchModeHint.textContent = isImageMode
|
||||||
? "GIPHY 이미지/GIF 검색 모드입니다. Gemini가 영어 검색어 5개로 확장한 뒤 최대 100개 결과를 보여줍니다."
|
? "GIPHY 이미지/GIF 검색 결과를 그대로 보여주는 모드입니다. 최대 100개 결과를 내부 스크롤로 탐색할 수 있습니다."
|
||||||
: "비디오 검색 모드입니다. 실제 검색 API와 연결되어 있습니다.";
|
: "비디오 검색 모드입니다. 실제 검색 API와 연결되어 있습니다.";
|
||||||
searchQuery.placeholder = isImageMode ? "검색할 이미지를 설명하세요" : "한글 검색어를 입력하세요";
|
searchQuery.placeholder = isImageMode ? "검색할 이미지를 설명하세요" : "한글 검색어를 입력하세요";
|
||||||
searchSubmitButton.textContent = isImageMode ? "Search GIPHY" : "AI Search";
|
searchSubmitButton.textContent = isImageMode ? "Search GIPHY" : "AI Search";
|
||||||
@@ -376,7 +349,6 @@ function applyMediaTypeUI() {
|
|||||||
button.classList.toggle("hidden", isImageMode);
|
button.classList.toggle("hidden", isImageMode);
|
||||||
}
|
}
|
||||||
if (isImageMode) {
|
if (isImageMode) {
|
||||||
updateImageSearchMeta(null);
|
|
||||||
setStatus("giphy image mode", 0);
|
setStatus("giphy image mode", 0);
|
||||||
renderImageEmptyState("GIPHY 검색어를 입력하면 여기에 최대 100개의 이미지/GIF 결과가 표시됩니다.");
|
renderImageEmptyState("GIPHY 검색어를 입력하면 여기에 최대 100개의 이미지/GIF 결과가 표시됩니다.");
|
||||||
} else {
|
} else {
|
||||||
@@ -847,15 +819,21 @@ async function openResultModal(item) {
|
|||||||
const summaryRequestId = activeResultModalSummaryRequest;
|
const summaryRequestId = activeResultModalSummaryRequest;
|
||||||
resultModalTitle.textContent = item.title || "Untitled";
|
resultModalTitle.textContent = item.title || "Untitled";
|
||||||
resultModalSource.textContent = item.source || "";
|
resultModalSource.textContent = item.source || "";
|
||||||
|
resultModalReasonLabel.textContent = giphyItem ? "Result Info" : "AI Note";
|
||||||
|
resultModalSnippetLabel.textContent = giphyItem ? "Source" : "Source Summary";
|
||||||
resultModalReason.textContent = giphyItem
|
resultModalReason.textContent = giphyItem
|
||||||
? [
|
? [
|
||||||
`Original Query: ${item.originalQuery || "-"}`,
|
|
||||||
`Expanded Query: ${item.searchQuery || "-"}`,
|
|
||||||
`Rating: ${item.rating || "unrated"}`,
|
`Rating: ${item.rating || "unrated"}`,
|
||||||
|
`Size: ${item.width || "?"} x ${item.height || "?"}`,
|
||||||
|
`Provider ID: ${item.providerId || "-"}`,
|
||||||
].join("\n")
|
].join("\n")
|
||||||
: (summarizeReason(item.reason) || "AI 노트가 없습니다.");
|
: (summarizeReason(item.reason) || "AI 노트가 없습니다.");
|
||||||
const originalSummary = giphyItem
|
const originalSummary = giphyItem
|
||||||
? `Powered by GIPHY\n${item.width || "?"} x ${item.height || "?"}\n${item.sourcePageUrl || item.openUrl || item.link || ""}`.trim()
|
? [
|
||||||
|
"Powered by GIPHY",
|
||||||
|
item.sourcePageUrl || item.openUrl || item.link || "",
|
||||||
|
`Rating: ${item.rating || "unrated"}`,
|
||||||
|
].filter(Boolean).join("\n")
|
||||||
: (item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.");
|
: (item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.");
|
||||||
resultModalSnippet.textContent = originalSummary;
|
resultModalSnippet.textContent = originalSummary;
|
||||||
resultModalOpenExternal.href = item.openUrl || item.sourcePageUrl || item.link || "#";
|
resultModalOpenExternal.href = item.openUrl || item.sourcePageUrl || item.link || "#";
|
||||||
@@ -930,13 +908,11 @@ searchForm.addEventListener("submit", async (event) => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ query: searchQuery.value, maxResults: 100 }),
|
body: JSON.stringify({ query: searchQuery.value, maxResults: 100 }),
|
||||||
});
|
});
|
||||||
updateImageSearchMeta(data);
|
|
||||||
renderImageResults(data.items || []);
|
renderImageResults(data.items || []);
|
||||||
showWarning(data.warning || "");
|
showWarning("");
|
||||||
logEvent("image-search:completed", { query: data.originalQuery || searchQuery.value, total: data.total || 0, expandedQueries: data.expandedQueries || [] });
|
logEvent("image-search:completed", { query: data.originalQuery || searchQuery.value, total: data.total || 0, expandedQueries: data.expandedQueries || [] });
|
||||||
setStatus("giphy search complete", 100);
|
setStatus("giphy search complete", 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
updateImageSearchMeta(null);
|
|
||||||
renderImageEmptyState("GIPHY 검색 결과를 불러오지 못했습니다.");
|
renderImageEmptyState("GIPHY 검색 결과를 불러오지 못했습니다.");
|
||||||
showWarning(error.message);
|
showWarning(error.message);
|
||||||
setStatus("giphy search failed", 100);
|
setStatus("giphy search failed", 100);
|
||||||
@@ -971,15 +947,6 @@ for (const button of mediaTypeToggles) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const chip of imagePromptChips) {
|
|
||||||
chip.addEventListener("click", () => {
|
|
||||||
searchQuery.value = chip.dataset.imagePrompt || "";
|
|
||||||
if (activeMediaType === "image") {
|
|
||||||
setStatus("image prompt applied", 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadFile(file) {
|
async function uploadFile(file) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|||||||
+3
-34
@@ -52,37 +52,6 @@
|
|||||||
<input id="searchQuery" type="text" placeholder="한글 검색어를 입력하세요" class="flex-1 rounded-2xl border border-white/10 bg-black/40 px-5 py-4 text-base text-white outline-none ring-0 placeholder:text-zinc-500" />
|
<input id="searchQuery" type="text" placeholder="한글 검색어를 입력하세요" class="flex-1 rounded-2xl border border-white/10 bg-black/40 px-5 py-4 text-base text-white outline-none ring-0 placeholder:text-zinc-500" />
|
||||||
<button id="searchSubmitButton" class="rounded-2xl border border-white bg-white px-7 py-4 text-base font-medium text-black transition hover:bg-zinc-200">AI Search</button>
|
<button id="searchSubmitButton" class="rounded-2xl border border-white bg-white px-7 py-4 text-base font-medium text-black transition hover:bg-zinc-200">AI Search</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="imageSearchSandbox" class="mt-4 hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,rgba(250,204,21,0.07),rgba(59,130,246,0.08))] p-4">
|
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">GIPHY Image Search</p>
|
|
||||||
<span class="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] uppercase tracking-[0.24em] text-zinc-300">Powered by GIPHY</span>
|
|
||||||
</div>
|
|
||||||
<p class="max-w-2xl text-sm leading-6 text-zinc-300">
|
|
||||||
어떤 언어로 검색해도 Gemini가 영어 검색어 5개로 확장한 뒤 GIPHY 이미지/GIF 검색을 수행합니다. 결과는 아래 내부 스크롤 패널에서 최대 100개까지 탐색할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<button type="button" class="image-prompt-chip rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-200" data-image-prompt="cinematic city night">Cinematic City</button>
|
|
||||||
<button type="button" class="image-prompt-chip rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-200" data-image-prompt="vintage fashion portrait">Vintage Portrait</button>
|
|
||||||
<button type="button" class="image-prompt-chip rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-200" data-image-prompt="minimal product mockup">Product Mockup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="giphyMetaPanel" class="mt-4 hidden rounded-2xl border border-white/10 bg-black/30 p-4">
|
|
||||||
<div class="grid gap-3 lg:grid-cols-[1.4fr_1fr]">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-xs uppercase tracking-[0.22em] text-zinc-500">Search Meta</p>
|
|
||||||
<p id="giphyOriginalQuery" class="text-sm text-zinc-200">Original query: -</p>
|
|
||||||
<p id="giphyResultCount" class="text-sm text-zinc-400">0 results</p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-xs uppercase tracking-[0.22em] text-zinc-500">Expanded Queries</p>
|
|
||||||
<div id="giphyExpandedQueries" class="flex flex-wrap gap-2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="searchWarning" class="mt-3 hidden rounded-2xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-200"></div>
|
<div id="searchWarning" class="mt-3 hidden rounded-2xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-200"></div>
|
||||||
<div id="queryVariants" class="hidden"></div>
|
<div id="queryVariants" class="hidden"></div>
|
||||||
<div id="searchResultsViewport" class="mt-6">
|
<div id="searchResultsViewport" class="mt-6">
|
||||||
@@ -221,7 +190,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="result-modal-details grid min-h-0 gap-3 px-3 py-3 sm:gap-4 sm:px-4 sm:py-4 lg:grid-cols-[1.5fr_0.8fr]">
|
<div class="result-modal-details grid min-h-0 gap-3 px-3 py-3 sm:gap-4 sm:px-4 sm:py-4 lg:grid-cols-[1.5fr_0.8fr]">
|
||||||
<div class="flex min-h-[180px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
<div class="flex min-h-[180px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
|
<p id="resultModalReasonLabel" class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
|
||||||
<div class="result-panel-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
|
<div class="result-panel-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||||
<p id="resultModalReason" class="whitespace-pre-wrap text-xs leading-6 text-zinc-200 sm:text-sm"></p>
|
<p id="resultModalReason" class="whitespace-pre-wrap text-xs leading-6 text-zinc-200 sm:text-sm"></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +205,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-h-0 flex-1 flex-col">
|
<div class="flex min-h-0 flex-1 flex-col">
|
||||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Source Summary</p>
|
<p id="resultModalSnippetLabel" class="text-xs uppercase tracking-[0.25em] text-zinc-500">Source Summary</p>
|
||||||
<div class="result-summary-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
|
<div class="result-summary-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||||
<p id="resultModalSnippet" class="text-xs leading-6 text-zinc-300 sm:text-sm"></p>
|
<p id="resultModalSnippet" class="text-xs leading-6 text-zinc-300 sm:text-sm"></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,6 +249,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/app.js?v=20260324a" defer></script>
|
<script src="/app.js?v=20260324b" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-2
@@ -15,7 +15,7 @@
|
|||||||
<Icon>https://raw.githubusercontent.com/selfhst/icons/main/png/google-gemini-light.png</Icon>
|
<Icon>https://raw.githubusercontent.com/selfhst/icons/main/png/google-gemini-light.png</Icon>
|
||||||
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="Dashboard port" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="Dashboard port" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
||||||
<Config Name="Downloads" Target="/app/downloads" Default="/mnt/user/appdata/ai-media-hub/downloads" Mode="rw" Description="Media output directory" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/downloads</Config>
|
<Config Name="Downloads" Target="/app/downloads" Default="/mnt/user/appdata/ai-media-hub/downloads" Mode="rw" Description="Media output directory" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/downloads</Config>
|
||||||
<Config Name="GIPHY Downloads" Target="/downloads/giphy" Default="/mnt/user/appdata/ai-media-hub/giphy" Mode="rw" Description="Directory for downloaded GIPHY images and GIFs" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/giphy</Config>
|
<Config Name="GIPHY Downloads" Target="/app/downloads/giphy" Default="/mnt/user/appdata/ai-media-hub/giphy" Mode="rw" Description="Directory for downloaded GIPHY images and GIFs" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/giphy</Config>
|
||||||
<Config Name="Database" Target="/app/db" Default="/mnt/user/appdata/ai-media-hub/db" Mode="rw" Description="SQLite database directory" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/db</Config>
|
<Config Name="Database" Target="/app/db" Default="/mnt/user/appdata/ai-media-hub/db" Mode="rw" Description="SQLite database directory" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ai-media-hub/db</Config>
|
||||||
<Config Name="SearXNG Base URL" Target="SEARXNG_BASE_URL" Default="http://searxng:8080" Mode="" Description="Base URL for the SearXNG instance" Type="Variable" Display="always" Required="true" Mask="false">http://searxng:8080</Config>
|
<Config Name="SearXNG Base URL" Target="SEARXNG_BASE_URL" Default="http://searxng:8080" Mode="" Description="Base URL for the SearXNG instance" Type="Variable" Display="always" Required="true" Mask="false">http://searxng:8080</Config>
|
||||||
<Config Name="SearXNG Google Video Engine" Target="SEARXNG_GOOGLE_VIDEO_ENGINE" Default="google videos" Mode="" Description="Engine name used for Google video searches" Type="Variable" Display="always" Required="true" Mask="false">google videos</Config>
|
<Config Name="SearXNG Google Video Engine" Target="SEARXNG_GOOGLE_VIDEO_ENGINE" Default="google videos" Mode="" Description="Engine name used for Google video searches" Type="Variable" Display="always" Required="true" Mask="false">google videos</Config>
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
<Config Name="GIPHY Max Results" Target="GIPHY_MAX_RESULTS" Default="100" Mode="" Description="Maximum number of aggregated GIPHY results to return" Type="Variable" Display="always" Required="true" Mask="false">100</Config>
|
<Config Name="GIPHY Max Results" Target="GIPHY_MAX_RESULTS" Default="100" Mode="" Description="Maximum number of aggregated GIPHY results to return" Type="Variable" Display="always" Required="true" Mask="false">100</Config>
|
||||||
<Config Name="GIPHY Rating" Target="GIPHY_RATING" Default="g" Mode="" Description="GIPHY content rating filter" Type="Variable" Display="always" Required="true" Mask="false">g</Config>
|
<Config Name="GIPHY Rating" Target="GIPHY_RATING" Default="g" Mode="" Description="GIPHY content rating filter" Type="Variable" Display="always" Required="true" Mask="false">g</Config>
|
||||||
<Config Name="GIPHY Lang" Target="GIPHY_LANG" Default="en" Mode="" Description="Language hint sent to GIPHY search" Type="Variable" Display="always" Required="true" Mask="false">en</Config>
|
<Config Name="GIPHY Lang" Target="GIPHY_LANG" Default="en" Mode="" Description="Language hint sent to GIPHY search" Type="Variable" Display="always" Required="true" Mask="false">en</Config>
|
||||||
<Config Name="GIPHY Download Dir" Target="GIPHY_DOWNLOAD_DIR" Default="/downloads/giphy" Mode="" Description="Container path used for saved GIPHY media" Type="Variable" Display="always" Required="true" Mask="false">/downloads/giphy</Config>
|
|
||||||
<Config Name="Gemini API Key" Target="GEMINI_API_KEY" Default="" Mode="" Description="Gemini API key" Type="Variable" Display="always" Required="false" Mask="true"/>
|
<Config Name="Gemini API Key" Target="GEMINI_API_KEY" Default="" Mode="" Description="Gemini API key" Type="Variable" Display="always" Required="false" Mask="true"/>
|
||||||
<Config Name="Gemini Model" Target="GEMINI_MODEL" Default="gemini-2.5-flash" Mode="" Description="Gemini model used for multilingual query expansion" Type="Variable" Display="always" Required="true" Mask="false">gemini-2.5-flash</Config>
|
<Config Name="Gemini Model" Target="GEMINI_MODEL" Default="gemini-2.5-flash" Mode="" Description="Gemini model used for multilingual query expansion" Type="Variable" Display="always" Required="true" Mask="false">gemini-2.5-flash</Config>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
Reference in New Issue
Block a user