const state = { projects: [], selectedProject: null, projectDetail: null, ontology: null, siteCrawlPoll: null, siteCrawlPageCount: 0, activeSiteCrawlJobId: null, }; const $ = (id) => document.getElementById(id); function csv(value) { return value.split(",").map((item) => item.trim()).filter(Boolean); } async function api(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); if (!response.ok) { let detail = `${response.status} ${response.statusText}`; try { const body = await response.json(); detail = body.detail || body.error || detail; } catch { // Keep status text. } throw new Error(detail); } return response.json(); } function toast(message) { const node = $("toast"); node.textContent = message; node.classList.add("show"); window.setTimeout(() => node.classList.remove("show"), 2400); } function requestBase() { return { config_path: $("configPath").value.trim(), source_name: $("sourceSelect").value, url: $("crawlUrl").value.trim(), extractor_provider: $("extractorProvider").value, extractor_model: $("extractorModel").value.trim() || null, extractor_base_url: $("extractorBaseUrl").value.trim() || null, }; } async function loadProjects() { state.projects = await api("/projects"); if (!state.selectedProject && state.projects.length) { state.selectedProject = state.projects[0].name; } renderProjects(); if (state.selectedProject) { await selectProject(state.selectedProject); } } function renderProjects() { const list = $("projectList"); list.innerHTML = ""; state.projects.forEach((project) => { const button = document.createElement("button"); button.className = `project-item ${state.selectedProject === project.name ? "active" : ""}`; button.innerHTML = `${escapeHtml(project.name)}${escapeHtml(project.domain)}`; button.addEventListener("click", () => selectProject(project.name)); list.appendChild(button); }); } async function selectProject(projectName) { state.selectedProject = projectName; state.projectDetail = await api(`/projects/${encodeURIComponent(projectName)}`); state.ontology = await api(`/ontology/${encodeURIComponent(state.projectDetail.domain)}`); renderProjects(); renderOverview(); renderOntology(); await Promise.all([ loadOntologyRegistry(), loadEntities(), loadClaims(), loadExtractionLogs(), loadResearchSessions(), loadGraphQuery(), loadTags(), ]); } function renderOverview() { const detail = state.projectDetail; $("metricProject").textContent = detail?.name ?? "-"; $("metricDomain").textContent = detail?.domain ?? "-"; $("metricSources").textContent = detail?.sources?.length ?? 0; const sourceSelect = $("sourceSelect"); sourceSelect.innerHTML = ""; (detail?.sources ?? []).forEach((source) => { const option = document.createElement("option"); option.value = source.name; option.textContent = `${source.name} (${source.type})`; sourceSelect.appendChild(option); }); $("sourceTable").innerHTML = table( ["Name", "Type", "Trust", "Robots", "Rate"], (detail?.sources ?? []).map((source) => [ source.name, source.type, source.trust_level, source.respect_robots_txt ? "on" : "off", `${source.rate_limit_per_minute}/min`, ]) ); } function renderOntology() { const entityTypes = state.ontology?.entity_types ?? []; const predicates = state.ontology?.predicates ?? []; $("ontologyEntities").innerHTML = entityTypes.map(chip).join(""); $("ontologyPredicates").innerHTML = predicates.map(chip).join(""); $("entityTypeFilter").innerHTML = `${entityTypes .map((type) => ``) .join("")}`; } async function loadOntologyRegistry() { if (!state.selectedProject) return; const [registry, proposals, gaps, triples] = await Promise.all([ api(`/projects/${encodeURIComponent(state.selectedProject)}/ontology/registry`), api(`/projects/${encodeURIComponent(state.selectedProject)}/ontology/proposals?limit=50`), api(`/projects/${encodeURIComponent(state.selectedProject)}/knowledge-gaps?limit=50`), api(`/projects/${encodeURIComponent(state.selectedProject)}/ontology/triples?limit=50`), ]); $("ontologyRegistryEntityTable").innerHTML = table( ["Type", "Domain", "Status", "Confidence"], (registry.entity_types ?? []).map((row) => [ row.name, row.domain, row.status, Math.round((row.confidence ?? 0) * 100) + "%", ]) ); $("ontologyRegistryRelationTable").innerHTML = table( ["Relation", "Domain", "Subject", "Object", "Status"], (registry.relation_types ?? []).map((row) => [ row.name, row.domain, (row.allowed_subject_types ?? []).join(", "), (row.allowed_object_types ?? []).join(", ") || (row.semantic_constraints?.literal_value ? "literal" : ""), row.status, ]) ); $("ontologyProposalTable").innerHTML = table( ["Type", "Name", "Status", "Reason"], (proposals ?? []).map((row) => [row.proposal_type, row.name, row.status, escapeHtml(row.reason ?? "")]) ); $("knowledgeGapTable").innerHTML = table( ["Gap", "Target", "Priority", "Description"], (gaps ?? []).map((row) => [ row.gap_type, `${row.target_type ?? ""}: ${row.target_name ?? ""}`, Math.round((row.priority ?? 0) * 100) + "%", escapeHtml(row.description ?? ""), ]) ); $("ontologyTripleTable").innerHTML = table( ["Subject", "Relation", "Object", "Status", "Support"], (triples ?? []).map((row) => [ row.subject ?? "", row.predicate, row.object ?? JSON.stringify(row.object_value ?? ""), row.status, row.support_count, ]) ); } async function createProject() { const configPath = $("configPath").value.trim(); if (!configPath) return; const result = await api("/projects", { method: "POST", body: JSON.stringify({ config_path: configPath }), }); toast(`Project created: ${result.name}`); state.selectedProject = result.name; await loadProjects(); } async function initializeCurrentProject() { const configPath = $("configPath").value.trim(); if (!configPath) { toast("Config path is required"); return; } if (!window.confirm("Reset current project crawl data (pages/entities/claims)?")) return; const result = await api("/projects/reset", { method: "POST", body: JSON.stringify({ config_path: configPath, project_name: state.selectedProject, }), }); state.selectedProject = result.name; await loadProjects(); if (result.reset) { const removedClaims = result.deleted?.claims ?? 0; const removedEntities = result.deleted?.entities ?? 0; const removedPages = result.deleted?.pages ?? 0; $("crawlResult").textContent = `Reset done: pages ${removedPages}, entities ${removedEntities}, claims ${removedClaims}`; toast(`Reset completed: ${result.name}`); return; } $("crawlResult").textContent = `Project created: ${result.name}`; toast(`Project created: ${result.name}`); } async function crawl() { if (!state.selectedProject) return; $("crawlResult").textContent = "Crawling one URL..."; try { const result = await api("/crawl", { method: "POST", body: JSON.stringify(requestBase()), }); $("crawlResult").textContent = `URL done: page ${result.page_id}, ${result.crawl_status}/${result.extraction_status}, ${result.page_type}, raw ${result.raw_text_length}, clean ${result.clean_text_length}, claims ${result.claim_count}, entities ${result.entity_count}`; toast("URL crawl completed"); await Promise.all([ loadOntologyRegistry(), loadEntities(), loadClaims(), loadExtractionLogs(), loadResearchSessions(), loadGraphQuery(), loadTags(), ]); } catch (error) { $("crawlResult").textContent = `Crawl failed: ${error.message}`; toast("Crawl failed"); } } async function crawlSite() { if (!state.selectedProject) return; stopSiteCrawlPolling(); state.siteCrawlPageCount = 0; state.siteCrawlPollErrorCount = 0; $("crawlResult").textContent = "Starting site crawl from seed..."; $("discoveredLinks").innerHTML = ""; try { const job = await api("/crawl-site", { method: "POST", body: JSON.stringify({ ...requestBase(), max_depth: Number($("siteMaxDepth").value || 0), max_pages: Number($("siteMaxPages").value || 1), same_domain_only: $("sameDomainOnly").checked, analyze_page_types: ["ProductPage", "BrandStoryPage", "ReviewPage"], }), }); $("crawlResult").textContent = `Site crawl queued: job ${job.job_id}`; state.activeSiteCrawlJobId = job.job_id; $("stopSiteCrawlBtn").disabled = false; renderSiteCrawlProgress(job); pollSiteCrawl(job.job_id); toast("Site crawl started"); } catch (error) { $("crawlResult").textContent = `Site crawl failed: ${error.message}`; toast("Site crawl failed"); } } async function stopSiteCrawl() { const jobId = state.activeSiteCrawlJobId; if (!jobId) return; $("stopSiteCrawlBtn").disabled = true; $("crawlResult").textContent = `Stopping site crawl: job ${jobId}`; try { const job = await api(`/crawl-site/jobs/${jobId}/cancel`, { method: "POST" }); renderSiteCrawlProgress(job); toast("Stop requested"); } catch (error) { $("stopSiteCrawlBtn").disabled = false; $("crawlResult").textContent = `Stop failed: ${error.message}`; toast("Stop failed"); } } function stopSiteCrawlPolling() { if (state.siteCrawlPoll) { window.clearTimeout(state.siteCrawlPoll); state.siteCrawlPoll = null; } } async function pollSiteCrawl(jobId) { let job; try { job = await api(`/crawl-site/jobs/${jobId}`); } catch (error) { const transient = state.siteCrawlPollErrorCount = (state.siteCrawlPollErrorCount ?? 0) + 1; $("crawlResult").textContent = `Site crawl status check failed (retry ${transient}): ${error.message}`; if (transient >= 5) { stopSiteCrawlPolling(); toast("Site crawl status failed"); return; } state.siteCrawlPoll = window.setTimeout(() => pollSiteCrawl(jobId), 3000); return; } state.siteCrawlPollErrorCount = 0; const progress = job.progress ?? {}; const pageCount = progress.pages?.length ?? 0; renderSiteCrawlProgress(job); if (pageCount !== state.siteCrawlPageCount) { state.siteCrawlPageCount = pageCount; const loaders = [ ["ontology registry", loadOntologyRegistry], ["entities", loadEntities], ["claims", loadClaims], ["extraction logs", loadExtractionLogs], ["research sessions", loadResearchSessions], ["graph query", loadGraphQuery], ["tags", loadTags], ]; await Promise.all(loaders.map(async ([label, fn]) => { try { await fn(); } catch (error) { console.warn(`poll: ${label} reload failed`, error); } })); } if (["completed", "failed", "canceled"].includes(job.status)) { stopSiteCrawlPolling(); state.activeSiteCrawlJobId = null; $("stopSiteCrawlBtn").disabled = true; toast(job.status === "completed" ? "Site crawl completed" : `Site crawl ${job.status}`); return; } state.siteCrawlPoll = window.setTimeout(() => pollSiteCrawl(jobId), 1500); } function renderSiteCrawlProgress(job) { const progress = job.progress ?? {}; const pages = progress.pages ?? []; $("crawlResult").textContent = `Site ${job.status}: visited ${progress.visited_count ?? 0}, analyzed ${progress.analyzed_count ?? 0}, skipped ${progress.skipped_count ?? 0}, queued ${progress.queued_count ?? 0}`; $("discoveredLinks").innerHTML = pages.map(renderSitePage).join(""); document.querySelectorAll("[data-discovered-url]").forEach((button) => { button.addEventListener("click", () => { $("crawlUrl").value = button.dataset.discoveredUrl; toast("URL copied to input"); }); }); } function renderSitePage(page) { const diagnostics = `raw ${page.raw_text_length ?? 0}, clean ${page.clean_text_length ?? 0}, removed ${page.removed_noise_zones_count ?? 0}`; const warnings = (page.warnings ?? []).length ? `, warnings: ${(page.warnings ?? []).join("; ")}` : ""; return ` `; } async function discover() { $("crawlResult").textContent = "Discovering links..."; $("discoveredLinks").innerHTML = ""; try { const result = await api("/discover", { method: "POST", body: JSON.stringify({ config_path: $("configPath").value.trim(), source_name: $("sourceSelect").value, url: $("crawlUrl").value.trim(), limit: 30, }), }); if (!result.ok) { $("crawlResult").textContent = result.error ?? "Discovery failed"; return; } $("crawlResult").textContent = `Discovered ${result.links.length} links`; $("discoveredLinks").innerHTML = result.links.map(renderDiscoveredLink).join(""); document.querySelectorAll("[data-discovered-url]").forEach((button) => { button.addEventListener("click", () => { $("crawlUrl").value = button.dataset.discoveredUrl; toast("URL copied to input"); }); }); } catch (error) { $("crawlResult").textContent = `Discovery failed: ${error.message}`; toast("Discovery failed"); } } function renderDiscoveredLink(link) { return ` `; } function updateExtractorOptions() { const provider = $("extractorProvider").value; $("extractorOptions").classList.toggle("active", provider !== "rule_based"); if (provider === "ollama" && !$("extractorBaseUrl").value.trim()) { $("extractorBaseUrl").placeholder = "http://localhost:11434/api/chat"; } else if (provider === "lm_studio" && !$("extractorBaseUrl").value.trim()) { $("extractorBaseUrl").value = "http://localhost:1234/v1"; $("extractorBaseUrl").placeholder = "http://localhost:1234/v1"; } else { $("extractorBaseUrl").placeholder = "optional provider endpoint"; } } async function testExtractor(options = {}) { const announce = options.announce ?? true; const provider = $("extractorProvider").value; const baseUrl = $("extractorBaseUrl").value.trim(); if (announce) { $("crawlResult").textContent = "Testing analyzer..."; } const result = await api("/extractors/models", { method: "POST", body: JSON.stringify({ provider, base_url: baseUrl || null }), }); if (!result.ok) { if (announce) { $("crawlResult").textContent = `Analyzer failed: ${result.error}`; toast("Analyzer failed"); } return; } const models = result.models ?? []; if (models.length && !$("extractorModel").value.trim()) { $("extractorModel").value = models[0].id; } if (announce) { $("crawlResult").textContent = models.length ? `Analyzer connected. Models: ${models.map((model) => model.id).join(", ")}` : "Analyzer connected. No models returned."; toast("Analyzer connected"); } } async function connectDefaultAnalyzer() { if ($("extractorProvider").value !== "lm_studio") return; try { await testExtractor({ announce: false }); } catch { // LM Studio may not be running yet; keep the default fields ready. } } async function loadEntities() { if (!state.selectedProject) return; const type = $("entityTypeFilter").value; const query = type ? `?entity_type=${encodeURIComponent(type)}&limit=100` : "?limit=100"; const entities = await api(`/projects/${encodeURIComponent(state.selectedProject)}/entities${query}`); $("metricEntities").textContent = entities.length; $("entityTable").innerHTML = table( ["ID", "Type", "Name", "Metadata"], entities.map((entity) => [ entity.id, entity.type, entity.name, `${escapeHtml(JSON.stringify(entity.metadata ?? {}))}`, ]) ); } async function mergeEntities() { if (!state.selectedProject) return; const sourceId = Number($("mergeSourceId").value); const targetId = Number($("mergeTargetId").value); if (!sourceId || !targetId || sourceId === targetId) { toast("Check entity IDs"); return; } const result = await api("/entities/merge", { method: "POST", body: JSON.stringify({ project_name: state.selectedProject, source_entity_id: sourceId, target_entity_id: targetId, }), }); if (!result.ok) { toast(result.error ?? "Merge failed"); return; } toast("Entity merged"); $("mergeSourceId").value = ""; $("mergeTargetId").value = ""; await Promise.all([ loadOntologyRegistry(), loadEntities(), loadClaims(), loadExtractionLogs(), loadResearchSessions(), loadGraphQuery(), loadTags(), ]); } async function loadClaims() { if (!state.selectedProject) return; const includeCandidates = $("showCandidateClaims")?.checked ? "&include_candidates=true" : ""; const claims = await api(`/projects/${encodeURIComponent(state.selectedProject)}/claims?limit=100${includeCandidates}`); $("claimTable").innerHTML = claims.map(renderClaim).join(""); document.querySelectorAll("[data-save-claim]").forEach((button) => { button.addEventListener("click", () => updateClaimConfidence(button.dataset.saveClaim)); }); } function renderClaim(claim) { const object = claim.object ?? JSON.stringify(claim.object_value ?? ""); const breakdown = claim.confidence_breakdown ? `LLM ${Math.round((claim.confidence_breakdown.llm_confidence ?? 0) * 100)}%, evidence ${Math.round((claim.confidence_breakdown.evidence_confidence ?? 0) * 100)}%, ontology ${Math.round((claim.confidence_breakdown.ontology_confidence ?? 0) * 100)}%, stored ${Math.round((claim.confidence_breakdown.stored_confidence ?? claim.confidence) * 100)}%` : ""; return `
${escapeHtml(claim.subject)} ${escapeHtml(claim.predicate)} ${escapeHtml(object)} ${escapeHtml(claim.status ?? "active")} ${Math.round(claim.confidence * 100)}%
${escapeHtml(claim.evidence_text ?? "")}
${escapeHtml(claim.page_type ?? "")} / ${escapeHtml(claim.source_zone ?? "")} / ${escapeHtml(claim.source_selector ?? "")}
${escapeHtml(claim.validation_status ?? "")} / graph ${escapeHtml(claim.graph_merge_status ?? "")}${claim.graph_merge_reason ? `: ${escapeHtml(claim.graph_merge_reason)}` : ""}
${claim.review_required ? `review: ${escapeHtml(claim.review_reason ?? "required")}` : ""}
${escapeHtml(breakdown)}
${escapeHtml(claim.source)} · ${escapeHtml(claim.page_url ?? "")}
`; } async function updateClaimConfidence(claimId) { const confidence = Number($(`confidence-${claimId}`).value); const reason = $(`reason-${claimId}`).value.trim(); await api(`/claims/${claimId}/confidence`, { method: "PATCH", body: JSON.stringify({ confidence, reason }), }); toast("Claim updated"); await loadClaims(); } async function loadExtractionLogs() { if (!state.selectedProject) return; const logs = await api(`/projects/${encodeURIComponent(state.selectedProject)}/extraction-logs?limit=50`); $("extractionLogTable").innerHTML = logs.map(renderExtractionLog).join(""); } function renderExtractionLog(log) { const validation = log.validation ?? {}; const context = log.page_context ?? {}; const rejected = validation.rejected_claims ?? []; const rejectedPreview = rejected .slice(0, 3) .map((item) => `${item.predicate ?? ""}: ${item.reason ?? ""}`) .join(" | "); return `
${escapeHtml(log.extractor_name)} ${escapeHtml(log.provider)} ${escapeHtml(validation.claim_status ?? "")} accepted ${validation.accepted_claim_count ?? 0} rejected ${validation.rejected_claim_count ?? 0}
${escapeHtml(context.page_type ?? "")} / ${escapeHtml(context.crawl_status ?? "")} / ${escapeHtml(context.extraction_status ?? "")}
${escapeHtml(log.page_url ?? "")}
${escapeHtml(rejectedPreview)}
`; } async function runResearch() { if (!state.selectedProject) return; $("researchResult").textContent = "Running graph research..."; const seedEntityRaw = $("researchSeedEntityId").value.trim(); try { const result = await api("/research/run", { method: "POST", body: JSON.stringify({ ...requestBase(), project_name: state.selectedProject, seed_entity_id: seedEntityRaw ? Number(seedEntityRaw) : null, goal: $("researchGoal").value.trim() || "Semantic ontology exploration", max_depth: Number($("siteMaxDepth").value || 2), max_steps: Number($("researchMaxSteps").value || 8), max_branch: 8, min_relevance: Number($("researchMinRelevance").value || 0.35), same_domain_only: $("sameDomainOnly").checked, analyze_page_types: ["ProductPage", "BrandStoryPage", "ReviewPage"], }), }); $("researchResult").textContent = `Research ${result.status}: explored ${result.explored_count}, analyzed ${result.analyzed_count}, queued ${result.queued_count}, skipped ${result.skipped_count}`; toast("Research completed"); await Promise.all([ loadOntologyRegistry(), loadEntities(), loadClaims(), loadExtractionLogs(), loadResearchSessions(), loadGraphQuery(), loadTags(), ]); } catch (error) { $("researchResult").textContent = `Research failed: ${error.message}`; toast("Research failed"); } } async function loadResearchSessions() { if (!state.selectedProject) return; const sessions = await api(`/projects/${encodeURIComponent(state.selectedProject)}/research/sessions?limit=20`); $("researchSessions").innerHTML = sessions.map(renderResearchSession).join(""); } function renderResearchSession(session) { const history = session.history ?? []; const latest = history.length ? history[history.length - 1] : null; const memory = session.memory ?? {}; const conflicts = memory.conflicts ?? []; return `
${escapeHtml(session.name ?? "Research session")} ${escapeHtml(session.status ?? "")} history ${history.length} queue ${(session.queue ?? []).length} conflicts ${conflicts.length}
${escapeHtml(session.goal ?? "")}
seed ${escapeHtml(session.seed ?? "")}
${escapeHtml(latest?.outcome?.reason ?? latest?.outcome?.status ?? "")}
`; } async function loadGraphQuery() { if (!state.selectedProject) return; const kind = $("graphQueryKind")?.value ?? "trend_summary"; const text = $("graphQueryText")?.value.trim() ?? ""; let query = `kind=${encodeURIComponent(kind)}`; if (kind === "brand_products" && text) query += `&brand=${encodeURIComponent(text)}`; if (kind === "products_by_tag" && text) query += `&tag=${encodeURIComponent(text)}`; const rows = await api(`/projects/${encodeURIComponent(state.selectedProject)}/graph/query?${query}`); $("graphQueryTable").innerHTML = renderGraphRows(kind, rows); } function renderGraphRows(kind, rows) { if (kind === "brand_products") { return table( ["Product", "Type", "Brand", "Confidence"], rows.map((row) => [row.product, row.product_type, row.brand ?? "", Math.round(row.confidence * 100) + "%"]) ); } if (kind === "products_by_tag") { return table( ["Product", "Predicate", "Tag", "Confidence"], rows.map((row) => [row.product, row.predicate, row.tag, Math.round(row.confidence * 100) + "%"]) ); } if (kind === "relation_summary") { return table( ["Predicate", "Status", "Support", "Confidence"], rows.map((row) => [ row.predicate, row.status, row.support_count, Math.round((row.max_confidence ?? 0) * 100) + "%", ]) ); } if (kind === "entity_type_summary") { return table( ["Entity Type", "Count"], rows.map((row) => [row.entity_type, row.count]) ); } return table( ["Predicate", "Name", "Type", "Support", "Confidence"], rows.map((row) => [ row.predicate, row.name, row.entity_type, row.support_count, Math.round(row.max_confidence * 100) + "%", ]) ); } async function loadTags() { if (!state.selectedProject) return; const tags = await api(`/projects/${encodeURIComponent(state.selectedProject)}/recommendation-tags`); $("tagTable").innerHTML = table( ["Predicate", "Type", "Name", "Support", "Confidence"], tags.map((tag) => [ tag.predicate, tag.type, tag.name, tag.support_count, Math.round(tag.max_confidence * 100) + "%", ]) ); } async function recommend() { if (!state.selectedProject) return; const result = await api("/recommend", { method: "POST", body: JSON.stringify({ project_name: state.selectedProject, target_entity_type: state.projectDetail?.config?.recommendation?.target_entity_type ?? "Perfume", preferences: { preferred_notes: csv($("preferredNotes").value), avoided_notes: csv($("avoidedNotes").value), preferred_moods: csv($("preferredMoods").value), season_context: $("seasonContext").value.trim() || null, occasion_context: $("occasionContext").value.trim() || null, }, limit: 10, }), }); $("recommendTable").innerHTML = table( ["Name", "Type", "Score", "Reasons"], result.map((item) => [item.name, item.entity_type, item.score, item.reasons.join(", ")]) ); } function chip(value) { return `${escapeHtml(value)}`; } function table(headers, rows) { if (!rows.length) { return `
No data.
`; } return ` ${headers.map((header) => ``).join("")}${rows.map((row) => `${row.map((cell) => ``).join("")}`).join("")}
${escapeHtml(header)}
${String(cell)}
`; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((node) => node.classList.remove("active")); document.querySelectorAll(".tab-panel").forEach((node) => node.classList.remove("active")); tab.classList.add("active"); $(tab.dataset.tab).classList.add("active"); }); }); $("refreshBtn").addEventListener("click", loadProjects); $("createProjectBtn").addEventListener("click", createProject); $("initProjectBtn").addEventListener("click", initializeCurrentProject); $("discoverBtn").addEventListener("click", discover); $("crawlBtn").addEventListener("click", crawl); $("siteCrawlBtn").addEventListener("click", crawlSite); $("stopSiteCrawlBtn").addEventListener("click", stopSiteCrawl); $("extractorProvider").addEventListener("change", updateExtractorOptions); $("testExtractorBtn").addEventListener("click", testExtractor); $("loadEntitiesBtn").addEventListener("click", loadEntities); $("mergeEntitiesBtn").addEventListener("click", mergeEntities); $("loadClaimsBtn").addEventListener("click", loadClaims); $("showCandidateClaims").addEventListener("change", loadClaims); $("loadExtractionLogsBtn").addEventListener("click", loadExtractionLogs); $("runResearchBtn").addEventListener("click", runResearch); $("loadResearchBtn").addEventListener("click", loadResearchSessions); $("loadGraphQueryBtn").addEventListener("click", loadGraphQuery); $("loadTagsBtn").addEventListener("click", loadTags); $("loadOntologyRegistryBtn").addEventListener("click", loadOntologyRegistry); $("recommendBtn").addEventListener("click", recommend); updateExtractorOptions(); connectDefaultAnalyzer(); loadProjects().catch((error) => toast(error.message));