[crawler_platform 삭제]

This commit is contained in:
LASTA_DEV01\lasta
2026-05-20 13:21:08 +09:00
parent be717a8f9a
commit fc69dfd063
173 changed files with 307 additions and 1982 deletions

View File

@@ -0,0 +1,172 @@
import { api } from "./api.js";
import { state } from "./state.js";
import { $, escapeHtml } from "./utils.js";
import { t, applyI18n, onLangChange } from "./i18n.js";
import { refreshInspector } from "./inspector.js";
let debounceTimer = null;
let lastQuery = "";
let resultsCache = null;
export function mountGlobalSearch() {
const input = $("globalSearchInput");
const panel = $("globalSearchPanel");
applyI18n(input.parentElement);
input.addEventListener("input", () => {
const q = input.value.trim();
if (debounceTimer) window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(() => runSearch(q), 200);
});
input.addEventListener("focus", () => {
if (lastQuery) renderPanel(resultsCache); else renderHint();
panel.hidden = false;
});
input.addEventListener("keydown", (event) => {
if (event.key === "Escape") { input.value = ""; panel.hidden = true; input.blur(); }
if (event.key === "ArrowDown") {
event.preventDefault();
const first = panel.querySelector("[data-search-item]");
first?.focus();
}
});
document.addEventListener("click", (event) => {
if (!panel.contains(event.target) && event.target !== input) panel.hidden = true;
});
document.addEventListener("keydown", (event) => {
if (event.key === "/" && !event.target.matches("input, textarea, select")) {
event.preventDefault();
input.focus();
input.select();
}
});
onLangChange(() => {
applyI18n(input.parentElement);
if (resultsCache) renderPanel(resultsCache);
});
}
async function runSearch(q) {
if (!state.selectedProject) return;
lastQuery = q;
const panel = $("globalSearchPanel");
if (!q) { renderHint(); return; }
try {
resultsCache = await api(`/projects/${encodeURIComponent(state.selectedProject)}/search?q=${encodeURIComponent(q)}&limit=8`);
renderPanel(resultsCache);
} catch (error) {
panel.innerHTML = `<div class="search-empty muted">${escapeHtml(error.message)}</div>`;
panel.hidden = false;
}
}
function renderHint() {
const panel = $("globalSearchPanel");
panel.innerHTML = `<div class="search-empty muted">${escapeHtml(t("search.hint"))}</div>`;
panel.hidden = false;
}
function renderPanel(results) {
const panel = $("globalSearchPanel");
if (!results) return;
const groups = [
{ key: "entities", items: results.entities, render: renderEntity },
{ key: "claims", items: results.claims, render: renderClaim },
{ key: "pages", items: results.pages, render: renderPage },
{ key: "predicates", items: results.predicates, render: renderPredicate },
].filter((g) => (g.items ?? []).length > 0);
if (!groups.length) {
panel.innerHTML = `<div class="search-empty muted">${escapeHtml(t("search.empty"))}</div>`;
} else {
panel.innerHTML = groups.map((g) => `
<div class="search-group">
<div class="search-group-head">${escapeHtml(t(`search.group.${g.key}`))}</div>
${g.items.map((item, idx) => g.render(item, idx)).join("")}
</div>
`).join("");
panel.querySelectorAll("[data-search-item]").forEach((node) => {
node.addEventListener("click", () => handleClick(node.dataset.searchItem, JSON.parse(node.dataset.payload)));
node.addEventListener("keydown", (event) => {
if (event.key === "Enter") { event.preventDefault(); node.click(); }
if (event.key === "ArrowDown") {
event.preventDefault();
(node.nextElementSibling || panel.querySelector("[data-search-item]"))?.focus?.();
}
if (event.key === "ArrowUp") {
event.preventDefault();
(node.previousElementSibling || panel.querySelector("[data-search-item]:last-of-type"))?.focus?.();
}
if (event.key === "Escape") {
$("globalSearchInput").focus();
}
});
});
}
panel.hidden = false;
}
function renderEntity(e) {
return `
<button class="search-item" data-search-item="entity" data-payload='${escapeHtmlAttr(JSON.stringify(e))}'>
<span class="search-item-main">${escapeHtml(e.name)}</span>
<span class="search-item-meta">${escapeHtml(e.type ?? "")}</span>
</button>
`;
}
function renderClaim(c) {
const obj = c.object_value ? JSON.stringify(c.object_value) : "";
return `
<button class="search-item" data-search-item="claim" data-payload='${escapeHtmlAttr(JSON.stringify(c))}'>
<span class="search-item-main">${escapeHtml(c.subject ?? "")} · ${escapeHtml(c.predicate ?? "")} ${escapeHtml(obj)}</span>
<span class="search-item-meta">${Math.round((c.confidence ?? 0) * 100)}%</span>
</button>
`;
}
function renderPage(p) {
return `
<button class="search-item" data-search-item="page" data-payload='${escapeHtmlAttr(JSON.stringify(p))}'>
<span class="search-item-main">${escapeHtml(p.title ?? p.url ?? "")}</span>
<span class="search-item-meta">${escapeHtml(p.url ?? "")}</span>
</button>
`;
}
function renderPredicate(name) {
return `
<button class="search-item" data-search-item="predicate" data-payload='${escapeHtmlAttr(JSON.stringify(name))}'>
<span class="search-item-main">${escapeHtml(name)}</span>
</button>
`;
}
function escapeHtmlAttr(value) {
return String(value).replaceAll("&", "&amp;").replaceAll("'", "&#039;").replaceAll('"', "&quot;");
}
function handleClick(kind, payload) {
const panel = $("globalSearchPanel");
panel.hidden = true;
if (kind === "entity") {
activateTab("entities");
state.selection = { kind: "entity", data: { id: payload.id, name: payload.name, type: payload.type, metadata: {} } };
refreshInspector();
} else if (kind === "claim") {
activateTab("claims");
state.selection = { kind: "claim", data: { ...payload, object: undefined } };
refreshInspector();
} else if (kind === "page") {
if (payload.url) window.open(payload.url, "_blank", "noreferrer");
} else if (kind === "predicate") {
activateTab("graph");
const sel = $("graphPredicate");
if (sel) { sel.value = payload; sel.dispatchEvent(new Event("change")); }
}
}
function activateTab(name) {
document.querySelectorAll(".tab").forEach((n) => n.classList.toggle("active", n.dataset.tab === name));
document.querySelectorAll(".tab-panel").forEach((n) => n.classList.toggle("active", n.id === name));
}