import { MAX_SEARCH_RESULTS, apiPath, fetchJson, matchesFilters, renderWorkList, selectedAges, splitFilters, updateDatasetStatus, } from "./shared.js"; const state = { naturalResults: null, requestId: 0, }; const elements = { datasetStatus: document.querySelector("#datasetStatus"), searchInput: document.querySelector("#searchInput"), naturalSearchButton: document.querySelector("#naturalSearchButton"), clearNaturalSearchButton: document.querySelector("#clearNaturalSearchButton"), searchMode: document.querySelector("#searchMode"), tagInput: document.querySelector("#tagInput"), ageFilters: document.querySelectorAll(".ageFilter"), workList: document.querySelector("#workList"), searchCount: document.querySelector("#searchCount"), }; function currentFilters() { return { ages: selectedAges(elements.ageFilters), tags: splitFilters(elements.tagInput.value), query: elements.searchInput.value, }; } async function normalResults(requestId) { const filters = currentFilters(); const payload = await fetchJson(apiPath("/api/works", { q: filters.query, limit: MAX_SEARCH_RESULTS, age: [...filters.ages], tag: filters.tags, })); if (requestId !== state.requestId) return null; return payload.results || []; } function naturalResults() { if (!state.naturalResults) return null; const filters = currentFilters(); return state.naturalResults .map((result) => ({ work: result.work, score: result.score })) .filter((entry) => entry.work && matchesFilters(entry.work, { ...filters, query: "" })); } async function renderSearch() { const semanticEntries = naturalResults(); if (semanticEntries) { renderWorkList(elements.workList, semanticEntries, { emptyText: "自然文検索に該当する作品がありません。" }); elements.searchCount.textContent = `${semanticEntries.length}件`; return; } const requestId = state.requestId + 1; state.requestId = requestId; elements.searchCount.textContent = "読み込み中"; renderWorkList(elements.workList, [], { emptyText: "最近追加されたASMRを読み込み中..." }); try { const results = await normalResults(requestId); if (!results) return; renderWorkList(elements.workList, results, { emptyText: "該当する作品がありません。" }); elements.searchCount.textContent = `${results.length}件`; elements.searchMode.textContent = elements.searchInput.value.trim() ? "作品名・タグ・声優名で絞り込み中です。" : "最近追加されたASMRを表示しています。"; } catch (error) { if (requestId !== state.requestId) return; elements.searchCount.textContent = "エラー"; renderWorkList(elements.workList, [], { emptyText: `作品一覧を読み込めません: ${error.message}` }); } } function clearNaturalSearch() { state.naturalResults = null; elements.clearNaturalSearchButton.hidden = true; elements.searchMode.textContent = "入力すると作品名・タグ・声優名で絞り込みます。"; renderSearch(); } async function requestNaturalSearch() { const query = elements.searchInput.value.trim(); if (!query) { elements.searchMode.textContent = "聴きたい雰囲気を入力してください。"; return; } elements.naturalSearchButton.disabled = true; elements.searchMode.textContent = "自然文検索中..."; try { const payload = await fetchJson("/api/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query, limit: MAX_SEARCH_RESULTS, ages: [...selectedAges(elements.ageFilters)], requiredTags: splitFilters(elements.tagInput.value), }), }); state.naturalResults = payload.results || []; elements.clearNaturalSearchButton.hidden = false; elements.searchMode.textContent = `自然文検索: ${query}`; renderSearch(); } catch (error) { state.naturalResults = null; elements.clearNaturalSearchButton.hidden = true; elements.searchMode.textContent = `自然文検索を使えません: ${error.message}`; renderSearch(); } finally { elements.naturalSearchButton.disabled = false; } } function bindEvents() { elements.searchInput.addEventListener("input", () => { if (state.naturalResults) clearNaturalSearch(); else renderSearch(); }); elements.searchInput.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; event.preventDefault(); requestNaturalSearch(); }); elements.naturalSearchButton.addEventListener("click", requestNaturalSearch); elements.clearNaturalSearchButton.addEventListener("click", clearNaturalSearch); elements.tagInput.addEventListener("input", renderSearch); for (const input of elements.ageFilters) input.addEventListener("change", renderSearch); } async function init() { const status = await fetchJson("/api/status"); updateDatasetStatus(elements.datasetStatus, status); bindEvents(); renderSearch(); } init().catch((error) => { elements.datasetStatus.textContent = `読み込み失敗: ${error.message}`; });