153 lines
5.0 KiB
JavaScript
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}`;
|
|
});
|