Files
asmr-vector-search/web/src/scripts/search.js
T
2026-06-11 03:43:59 +09:00

153 lines
5.0 KiB
JavaScript

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}`;
});