928 lines
513 KiB
JavaScript
928 lines
513 KiB
JavaScript
|
|
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))a(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&a(s)}).observe(document,{childList:!0,subtree:!0});function r(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function a(n){if(n.ep)return;n.ep=!0;const i=r(n);fetch(n.href,i)}})();const od="ocp.lang",ld=["ko","en"],ud="ko",Sl={ko:{"app.title":"Ontology Crawler","app.subtitle":"프로젝트 크롤, 온톨로지 매핑, 클레임 리뷰, 추천 태그","header.refresh":"새로고침","header.lang.ko":"한","header.lang.en":"EN","sidebar.projects":"프로젝트","sidebar.config_path":"설정 파일 경로","sidebar.create_project":"프로젝트 생성","sidebar.reset_project":"현재 프로젝트 데이터 리셋","sidebar.crawl":"크롤","sidebar.source":"소스","sidebar.url_or_seed":"URL / 시드 URL","sidebar.analyzer":"분석기","sidebar.model":"모델","sidebar.base_url":"Base URL","sidebar.test_analyzer":"분석기 테스트","sidebar.discover":"링크 탐색","sidebar.crawl_url":"URL 크롤","sidebar.max_depth":"최대 깊이","sidebar.max_pages":"최대 페이지","sidebar.same_domain":"같은 도메인만","sidebar.crawl_site":"시드부터 사이트 크롤","sidebar.stop_crawl":"현재 크롤 중지","tabs.overview":"개요","tabs.graph":"그래프","tabs.ontology":"온톨로지","tabs.entities":"엔티티","tabs.claims":"클레임","tabs.debug":"디버그","tabs.research":"리서치","tabs.tags":"태그","tabs.recommend":"추천","graph.search_placeholder":"엔티티명 검색...","graph.entity_type_all":"전체 타입","graph.predicate_all":"전체 관계","graph.min_confidence":"최소 신뢰도","graph.layout":"레이아웃","graph.layout.cose":"Force","graph.layout.circle":"원형","graph.layout.concentric":"동심원","graph.layout.grid":"그리드","graph.layout.breadthfirst":"트리","graph.reload":"그래프 새로고침","graph.fit":"맞춤","graph.empty":"아직 데이터가 없습니다. 크롤이 클레임을 만들면 그래프에 노드가 나타납니다.","graph.stats.nodes":"노드","graph.stats.edges":"엣지","graph.stats.shown":"표시","overview.project":"프로젝트","overview.domain":"도메인","overview.sources":"소스","overview.entities":"엔티티","overview.sources_heading":"소스","pipeline.heading":"파이프라인","pipeline.refresh":"새로고침","pipeline.stage.crawled":"크롤됨","pipeline.stage.extracted":"추출됨","pipeline.stage.claims":"클레임","pipeline.stage.validated":"승인됨","pipeline.stage.graph":"그래프 트리플","pipeline.retained":"유지율","pipeline.entity_types_heading":"엔티티 타입 분포","pipeline.recent_pages":"최근 페이지","pipeline.recent_claims":"최근 클레임","pipeline.empty.pages":"아직 크롤된 페이지가 없습니다.","pipeline.empty.claims":"아직 생성된 클레임이 없습니다.","search.placeholder":"엔티티 · 클레임 · 페이지 검색...","search.aria_label":"전역 검색","search.group.entities":"엔티티","search.group.claims":"클레임","search.group.pages":"페이지","search.group.predicates":"술어","search.empty":"결과 없음","search.hint":"최소 1글자 이상 입력하세요","table.name":"이름","table.type":"타입","table.trust":"신뢰도","table.robots":"robots","table.rate":"속도제한","table.domain":"도메인","table.status":"상태","table.confidence":"신뢰도","table.relation":"관계","table.subject":"주체","table.object":"객체","table.support":"근거 수","table.predicate":"술어","table.tag":"태그","table.brand":"브랜드","table.product":"상품","table.count":"수","table.no_data":"데이터 없음.","table.id":"ID","table.metadat
|
||
|
|
<header class="topbar">
|
||
|
|
<div class="topbar-brand">
|
||
|
|
<h1 data-i18n="app.title"></h1>
|
||
|
|
<p data-i18n="app.subtitle"></p>
|
||
|
|
</div>
|
||
|
|
<div class="topbar-actions">
|
||
|
|
<div class="global-search">
|
||
|
|
<svg viewBox="0 0 24 24" width="14" height="14" class="global-search-icon" aria-hidden="true">
|
||
|
|
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||
|
|
d="M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm10 2-4.35-4.35"/>
|
||
|
|
</svg>
|
||
|
|
<input id="globalSearchInput" type="search" autocomplete="off"
|
||
|
|
data-i18n-placeholder="search.placeholder" data-i18n-aria-label="search.aria_label" />
|
||
|
|
<div id="globalSearchPanel" class="global-search-panel" hidden></div>
|
||
|
|
</div>
|
||
|
|
<div class="lang-toggle" role="group" aria-label="Language">
|
||
|
|
<button class="lang-btn" data-lang="ko" data-i18n="header.lang.ko"></button>
|
||
|
|
<button class="lang-btn" data-lang="en" data-i18n="header.lang.en"></button>
|
||
|
|
</div>
|
||
|
|
<button id="refreshBtn" class="icon-button" data-i18n-title="header.refresh" data-i18n-aria-label="header.refresh">
|
||
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" width="18" height="18">
|
||
|
|
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||
|
|
d="M3 12a9 9 0 0 1 15.5-6.3L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.5 6.3L3 16M3 21v-5h5"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<main class="layout">
|
||
|
|
<aside id="sidebarHost" class="sidebar"></aside>
|
||
|
|
<section id="workspaceHost" class="workspace"></section>
|
||
|
|
<aside id="inspectorHost" class="inspector"></aside>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<div id="toast" role="status" aria-live="polite"></div>
|
||
|
|
`,document.documentElement.setAttribute("lang",kl()),xt(t);const e=t.querySelectorAll(".lang-btn"),r=()=>{const a=kl();e.forEach(n=>{n.classList.toggle("active",n.dataset.lang===a)})};return e.forEach(a=>{a.addEventListener("click",()=>Dv(a.dataset.lang))}),r(),fr(r),{sidebarHost:document.getElementById("sidebarHost"),workspaceHost:document.getElementById("workspaceHost"),inspectorHost:document.getElementById("inspectorHost")}}async function Fe(t,e={}){const r=await fetch(t,{headers:{"Content-Type":"application/json"},...e});if(!r.ok){let a=`${r.status} ${r.statusText}`;try{const n=await r.json();a=n.detail||n.error||a}catch{}throw new Error(a)}return r.json()}const ue={projects:[],selectedProject:null,projectDetail:null,ontology:null,siteCrawlPoll:null,siteCrawlPageCount:0,siteCrawlPollErrorCount:0,activeSiteCrawlJobId:null,latestActivity:null,selection:null},q=t=>document.getElementById(t);function Di(t){return String(t||"").split(",").map(e=>e.trim()).filter(Boolean)}function z(t){return String(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function Pl(t){return`<span class="chip">${z(t)}</span>`}function Mt(t,e){return e.length?`
|
||
|
|
<table>
|
||
|
|
<thead><tr>${t.map(r=>`<th>${z(r)}</th>`).join("")}</tr></thead>
|
||
|
|
<tbody>${e.map(r=>`<tr>${r.map(a=>`<td>${String(a??"")}</td>`).join("")}</tr>`).join("")}</tbody>
|
||
|
|
</table>
|
||
|
|
`:`<table><tbody><tr><td>${z(V("table.no_data"))}</td></tr></tbody></table>`}let Ri=null;function Ae(t){const e=document.getElementById("toast");e&&(e.textContent=t,e.classList.add("show"),Ri&&window.clearTimeout(Ri),Ri=window.setTimeout(()=>e.classList.remove("show"),2400))}function Av(t,{onProjectChanged:e,onCrawlUpdate:r,refreshAll:a}){t.innerHTML=Lv(),xt(t),q("createProjectBtn").addEventListener("click",()=>_v(e)),q("initProjectBtn").addEventListener("click",()=>Iv(e)),q("discoverBtn").addEventListener("click",Fv),q("crawlBtn").addEventListener("click",()=>Ov(a)),q("siteCrawlBtn").addEventListener("click",()=>Nv(r,a)),q("stopSiteCrawlBtn").addEventListener("click",zv),q("extractorProvider").addEventListener("change",Bl),q("testExtractorBtn").addEventListener("click",()=>dd({announce:!0})),Bl(),Vv(),fr(()=>{xt(t),Go(),si(cd)})}function Lv(){return`
|
||
|
|
<section class="panel">
|
||
|
|
<div class="panel-head"><h2 data-i18n="sidebar.projects"></h2></div>
|
||
|
|
<div class="field-row">
|
||
|
|
<input id="configPath" value="configs/perfume_subscription.yaml" data-i18n-aria-label="sidebar.config_path" />
|
||
|
|
<button id="createProjectBtn" data-i18n-title="sidebar.create_project">+</button>
|
||
|
|
</div>
|
||
|
|
<button id="initProjectBtn" class="full" data-i18n="sidebar.reset_project"></button>
|
||
|
|
<div id="projectList" class="list"></div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel">
|
||
|
|
<div class="panel-head"><h2 data-i18n="sidebar.crawl"></h2></div>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.source"></span>
|
||
|
|
<select id="sourceSelect"></select>
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.url_or_seed"></span>
|
||
|
|
<input id="crawlUrl" value="tests/fixtures/sample_perfume.html" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.analyzer"></span>
|
||
|
|
<select id="extractorProvider">
|
||
|
|
<option value="rule_based" data-i18n="extractor.rule_based"></option>
|
||
|
|
<option value="openai" data-i18n="extractor.openai"></option>
|
||
|
|
<option value="ollama" data-i18n="extractor.ollama"></option>
|
||
|
|
<option value="lm_studio" selected data-i18n="extractor.lm_studio"></option>
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<div class="extractor-options" id="extractorOptions">
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.model"></span>
|
||
|
|
<input id="extractorModel" placeholder="local or API model" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.base_url"></span>
|
||
|
|
<input id="extractorBaseUrl" value="http://localhost:1234/v1" placeholder="optional provider endpoint" />
|
||
|
|
</label>
|
||
|
|
<button id="testExtractorBtn" data-i18n="sidebar.test_analyzer"></button>
|
||
|
|
</div>
|
||
|
|
<div class="button-grid">
|
||
|
|
<button id="discoverBtn" data-i18n="sidebar.discover"></button>
|
||
|
|
<button id="crawlBtn" class="primary" data-i18n="sidebar.crawl_url"></button>
|
||
|
|
</div>
|
||
|
|
<div class="site-controls">
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.max_depth"></span>
|
||
|
|
<input id="siteMaxDepth" type="number" min="0" max="5" value="2" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="sidebar.max_pages"></span>
|
||
|
|
<input id="siteMaxPages" type="number" min="1" max="500" value="25" />
|
||
|
|
</label>
|
||
|
|
<label class="check-row">
|
||
|
|
<input id="sameDomainOnly" type="checkbox" checked />
|
||
|
|
<span data-i18n="sidebar.same_domain"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<button id="siteCrawlBtn" class="primary full" data-i18n="sidebar.crawl_site"></button>
|
||
|
|
<button id="stopSiteCrawlBtn" class="full" disabled data-i18n="sidebar.stop_crawl"></button>
|
||
|
|
<div id="crawlResult" class="mini-log"></div>
|
||
|
|
<div id="discoveredLinks" class="discovered-links"></div>
|
||
|
|
</section>
|
||
|
|
`}function Ho(){return{config_path:q("configPath").value.trim(),source_name:q("sourceSelect").value,url:q("crawlUrl").value.trim(),extractor_provider:q("extractorProvider").value,extractor_model:q("extractorModel").value.trim()||null,extractor_base_url:q("extractorBaseUrl").value.trim()||null}}function Go(){const t=q("projectList");t&&(t.innerHTML="",ue.projects.forEach(e=>{const r=document.createElement("button");r.className=`project-item ${ue.selectedProject===e.name?"active":""}`,r.innerHTML=`<strong>${z(e.name)}</strong><span>${z(e.domain)}</span>`,r.addEventListener("click",()=>{const a=new CustomEvent("project:select",{detail:e.name});window.dispatchEvent(a)}),t.appendChild(r)}))}function Mv(t){const e=q("sourceSelect");e&&(e.innerHTML="",(t?.sources??[]).forEach(r=>{const a=document.createElement("option");a.value=r.name,a.textContent=`${r.name} (${r.type})`,e.appendChild(a)}))}async function _v(t){const e=q("configPath").value.trim();if(e)try{const r=await Fe("/projects",{method:"POST",body:JSON.stringify({config_path:e})});Ae(`${V("toast.project_created")}: ${r.name}`),ue.selectedProject=r.name,await t?.()}catch(r){Ae(r.message)}}async function Iv(t){const e=q("configPath").value.trim();if(!e){Ae(V("toast.config_path_required"));return}if(window.confirm(V("toast.reset_confirm")))try{const r=await Fe("/projects/reset",{method:"POST",body:JSON.stringify({config_path:e,project_name:ue.selectedProject})});if(ue.selectedProject=r.name,await t?.(),r.reset){const a=r.deleted?.claims??0,n=r.deleted?.entities??0,i=r.deleted?.pages??0;q("crawlResult").textContent=`${V("toast.reset_done")}: pages ${i}, entities ${n}, claims ${a}`,Ae(`${V("toast.reset_done")}: ${r.name}`)}else q("crawlResult").textContent=`${V("toast.project_created")}: ${r.name}`,Ae(`${V("toast.project_created")}: ${r.name}`)}catch(r){Ae(r.message)}}async function Ov(t){if(ue.selectedProject){q("crawlResult").textContent=V("status.crawling_url");try{const e=await Fe("/crawl",{method:"POST",body:JSON.stringify(Ho())});q("crawlResult").textContent=`URL done: page ${e.page_id}, ${e.crawl_status}/${e.extraction_status}, ${e.page_type}, raw ${e.raw_text_length}, clean ${e.clean_text_length}, claims ${e.claim_count}, entities ${e.entity_count}`,ue.latestActivity={kind:"url_crawl",result:e},Ae(V("toast.url_crawl_completed")),await t?.()}catch(e){q("crawlResult").textContent=`${V("toast.crawl_failed")}: ${e.message}`,Ae(V("toast.crawl_failed"))}}}let cd=null;async function Nv(t,e){if(ue.selectedProject){fo(),ue.siteCrawlPageCount=0,ue.siteCrawlPollErrorCount=0,q("crawlResult").textContent=V("status.starting_site_crawl"),q("discoveredLinks").innerHTML="";try{const r=await Fe("/crawl-site",{method:"POST",body:JSON.stringify({...Ho(),max_depth:Number(q("siteMaxDepth").value||0),max_pages:Number(q("siteMaxPages").value||1),same_domain_only:q("sameDomainOnly").checked,analyze_page_types:["ProductPage","BrandStoryPage","ReviewPage"]})});q("crawlResult").textContent=`Site crawl queued: job ${r.job_id}`,ue.activeSiteCrawlJobId=r.job_id,q("stopSiteCrawlBtn").disabled=!1,si(r),vo(r.job_id,t,e),Ae(V("toast.site_crawl_started"))}catch(r){q("crawlResult").textContent=`${V("toast.site_crawl_failed")}: ${r.message}`,Ae(V("toast.site_crawl_failed"))}}}async function zv(){const t=ue.activeSiteCrawlJobId;if(t){q("stopSiteCrawlBtn").disabled=!0,q("crawlResult").textContent=`${V("status.stopping_crawl")}: job ${t}`;try{const e=await Fe(`/crawl-site/jobs/${t}/cancel`,{method:"POST"});si(e),Ae(V("toast.stop_requested"))}catch(e){q("stopSiteCrawlBtn").disabled=!1,q("crawlResult").textContent=`${V("toast.stop_failed")}: ${e.message}`,Ae(V("toast.stop_failed"))}}}function fo(){ue.siteCrawlPoll&&(window.clearTimeout(ue.siteCrawlPoll),ue.siteCrawlPoll=null)}async function vo(t,e,r){let a;try{a=await Fe(`/crawl-site/jobs/${t}`)}catch(s){const o=ue.siteCrawlPollErrorCount=(ue.siteCrawlPollErrorCount??0)+1;if(q("crawlResult").textContent=`Site crawl status check failed (retry ${o}): ${s.message}`,o>=5){fo(),Ae(V("toast.site_crawl_failed"));return}ue.siteCrawlPoll=window.setTimeout(()=>vo(t,
|
||
|
|
<button class="discovered-link" data-discovered-url="${z(t.url)}">
|
||
|
|
<strong>${z(t.status)} · ${z(t.page_type)} · depth ${t.depth}</strong>
|
||
|
|
<span>${z(t.url)}</span>
|
||
|
|
<span>${z(e)}${z(r)}</span>
|
||
|
|
<span>claims ${t.claim_count}, entities ${t.entity_count}, links ${t.discovered_count}${t.error?`, error: ${z(t.error)}`:""}</span>
|
||
|
|
</button>
|
||
|
|
`}async function Fv(){q("crawlResult").textContent=V("status.discovering"),q("discoveredLinks").innerHTML="";try{const t=await Fe("/discover",{method:"POST",body:JSON.stringify({config_path:q("configPath").value.trim(),source_name:q("sourceSelect").value,url:q("crawlUrl").value.trim(),limit:30})});if(!t.ok){q("crawlResult").textContent=t.error??V("toast.discovery_failed");return}q("crawlResult").textContent=`Discovered ${t.links.length} links`,q("discoveredLinks").innerHTML=t.links.map(qv).join(""),document.querySelectorAll("[data-discovered-url]").forEach(e=>{e.addEventListener("click",()=>{q("crawlUrl").value=e.dataset.discoveredUrl,Ae(V("toast.url_copied"))})})}catch(t){q("crawlResult").textContent=`${V("toast.discovery_failed")}: ${t.message}`,Ae(V("toast.discovery_failed"))}}function qv(t){return`
|
||
|
|
<button class="discovered-link" data-discovered-url="${z(t.url)}">
|
||
|
|
<strong>${z(t.label)}</strong>
|
||
|
|
<span>${z(t.kind)} · ${z(t.url)}</span>
|
||
|
|
</button>
|
||
|
|
`}function Bl(){const t=q("extractorProvider").value;q("extractorOptions").classList.toggle("active",t!=="rule_based"),t==="ollama"&&!q("extractorBaseUrl").value.trim()?q("extractorBaseUrl").placeholder="http://localhost:11434/api/chat":t==="lm_studio"&&!q("extractorBaseUrl").value.trim()?(q("extractorBaseUrl").value="http://localhost:1234/v1",q("extractorBaseUrl").placeholder="http://localhost:1234/v1"):q("extractorBaseUrl").placeholder="optional provider endpoint"}async function dd({announce:t=!0}={}){const e=q("extractorProvider").value,r=q("extractorBaseUrl").value.trim();t&&(q("crawlResult").textContent=V("status.testing_analyzer"));try{const a=await Fe("/extractors/models",{method:"POST",body:JSON.stringify({provider:e,base_url:r||null})});if(!a.ok){t&&(q("crawlResult").textContent=`${V("toast.analyzer_failed")}: ${a.error}`,Ae(V("toast.analyzer_failed")));return}const n=a.models??[];n.length&&!q("extractorModel").value.trim()&&(q("extractorModel").value=n[0].id),t&&(q("crawlResult").textContent=n.length?`${V("toast.analyzer_connected")}. Models: ${n.map(i=>i.id).join(", ")}`:`${V("toast.analyzer_connected")}. No models returned.`,Ae(V("toast.analyzer_connected")))}catch(a){t&&Ae(a.message)}}async function Vv(){if(q("extractorProvider").value==="lm_studio")try{await dd({announce:!1})}catch{}}let Rn=null;function Hv(t){Rn=t,An(),fr(An),window.addEventListener("inspector:update",An)}function ur(){An()}function An(){Rn&&(Rn.innerHTML=Gv(),xt(Rn))}function Gv(){const t=ue.selection;return`
|
||
|
|
<div class="inspector-head">
|
||
|
|
<h2 data-i18n="inspector.title"></h2>
|
||
|
|
<p class="muted" data-i18n="inspector.hint"></p>
|
||
|
|
</div>
|
||
|
|
${t?Wv(t):Uv()}
|
||
|
|
`}function Uv(){const t=ue.projectDetail,e=ue.latestActivity;return`
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3 data-i18n="inspector.project_info"></h3>
|
||
|
|
${t?`
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>${z(V("overview.project"))}</dt><dd>${z(t.name)}</dd>
|
||
|
|
<dt>${z(V("overview.domain"))}</dt><dd>${z(t.domain)}</dd>
|
||
|
|
<dt>${z(V("overview.sources"))}</dt><dd>${(t.sources??[]).length}</dd>
|
||
|
|
</dl>
|
||
|
|
`:'<p class="muted" data-i18n="inspector.no_selection"></p>'}
|
||
|
|
</section>
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3 data-i18n="inspector.latest_activity"></h3>
|
||
|
|
${e?Kv(e):'<p class="muted" data-i18n="inspector.latest_activity.empty"></p>'}
|
||
|
|
</section>
|
||
|
|
`}function Kv(t){if(t.kind==="site_crawl"){const e=t.job??{},r=e.progress??{};return`
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>job</dt><dd>${z(e.job_id??"-")}</dd>
|
||
|
|
<dt>status</dt><dd>${z(e.status??"-")}</dd>
|
||
|
|
<dt>visited</dt><dd>${r.visited_count??0}</dd>
|
||
|
|
<dt>analyzed</dt><dd>${r.analyzed_count??0}</dd>
|
||
|
|
<dt>skipped</dt><dd>${r.skipped_count??0}</dd>
|
||
|
|
<dt>queued</dt><dd>${r.queued_count??0}</dd>
|
||
|
|
</dl>
|
||
|
|
`}if(t.kind==="url_crawl"){const e=t.result??{};return`
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>page</dt><dd>${z(e.page_id??"-")}</dd>
|
||
|
|
<dt>page_type</dt><dd>${z(e.page_type??"-")}</dd>
|
||
|
|
<dt>crawl</dt><dd>${z(e.crawl_status??"-")}</dd>
|
||
|
|
<dt>extraction</dt><dd>${z(e.extraction_status??"-")}</dd>
|
||
|
|
<dt>claims</dt><dd>${e.claim_count??0}</dd>
|
||
|
|
<dt>entities</dt><dd>${e.entity_count??0}</dd>
|
||
|
|
</dl>
|
||
|
|
`}return""}function Wv(t){if(t.kind==="entity"){const e=t.data;return`
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3>${z(e.name)}</h3>
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>${z(V("table.id"))}</dt><dd>${z(e.id)}</dd>
|
||
|
|
<dt>${z(V("table.type"))}</dt><dd>${z(e.type)}</dd>
|
||
|
|
</dl>
|
||
|
|
<pre class="code">${z(JSON.stringify(e.metadata??{},null,2))}</pre>
|
||
|
|
</section>
|
||
|
|
`}if(t.kind==="claim"){const e=t.data;return`
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3>${z(e.subject)} · ${z(e.predicate)}</h3>
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>${z(V("table.object"))}</dt><dd>${z(e.object??JSON.stringify(e.object_value??""))}</dd>
|
||
|
|
<dt>${z(V("table.status"))}</dt><dd>${z(e.status??"")}</dd>
|
||
|
|
<dt>${z(V("table.confidence"))}</dt><dd>${Math.round((e.confidence??0)*100)}%</dd>
|
||
|
|
</dl>
|
||
|
|
${e.evidence_text?`<p class="evidence">${z(e.evidence_text)}</p>`:""}
|
||
|
|
</section>
|
||
|
|
`}if(t.kind==="graph_node"){const e=t.data;return`
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3>${z(e.label)}</h3>
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>${z(V("table.id"))}</dt><dd>${z(e.entityId)}</dd>
|
||
|
|
<dt>${z(V("table.type"))}</dt><dd>${z(e.type??"")}</dd>
|
||
|
|
${e.canonical?`<dt>canonical</dt><dd>${z(e.canonical)}</dd>`:""}
|
||
|
|
</dl>
|
||
|
|
${e.metadata&&Object.keys(e.metadata).length?`<pre class="code">${z(JSON.stringify(e.metadata,null,2))}</pre>`:""}
|
||
|
|
</section>
|
||
|
|
`}if(t.kind==="graph_edge"){const e=t.data;return`
|
||
|
|
<section class="inspector-section">
|
||
|
|
<h3>${z(e.predicate)}</h3>
|
||
|
|
<dl class="kv">
|
||
|
|
<dt>${z(V("table.confidence"))}</dt><dd>${Math.round((e.confidence??0)*100)}%</dd>
|
||
|
|
${e.targetName?`<dt>${z(V("table.object"))}</dt><dd>${z(e.targetName)}</dd>`:""}
|
||
|
|
<dt>claim</dt><dd>${z(e.claimId)}</dd>
|
||
|
|
</dl>
|
||
|
|
${e.metadata&&Object.keys(e.metadata).length?`<pre class="code">${z(JSON.stringify(e.metadata,null,2))}</pre>`:""}
|
||
|
|
</section>
|
||
|
|
`}return""}function ho(t,e){(e==null||e>t.length)&&(e=t.length);for(var r=0,a=Array(e);r<e;r++)a[r]=t[r];return a}function jv(t){if(Array.isArray(t))return t}function Yv(t){if(Array.isArray(t))return ho(t)}function Ar(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function Xv(t,e){for(var r=0;r<e.length;r++){var a=e[r];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(t,vd(a.key),a)}}function Lr(t,e,r){return e&&Xv(t.prototype,e),Object.defineProperty(t,"prototype",{writable:!1}),t}function Lt(t,e){var r=typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(!r){if(Array.isArray(t)||(r=Uo(t))||e){r&&(t=r);var a=0,n=function(){};return{s:n,n:function(){return a>=t.length?{done:!0}:{done:!1,value:t[a++]}},e:function(l){throw l},f:n}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.
|
||
|
|
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var i,s=!0,o=!1;return{s:function(){r=r.call(t)},n:function(){var l=r.next();return s=l.done,l},e:function(l){o=!0,i=l},f:function(){try{s||r.return==null||r.return()}finally{if(o)throw i}}}}function fd(t,e,r){return(e=vd(e))in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}function Zv(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}function Qv(t,e){var r=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(r!=null){var a,n,i,s,o=[],l=!0,u=!1;try{if(i=(r=r.call(t)).next,e===0){if(Object(r)!==r)return;l=!1}else for(;!(l=(a=i.call(r)).done)&&(o.push(a.value),o.length!==e);l=!0);}catch(c){u=!0,n=c}finally{try{if(!l&&r.return!=null&&(s=r.return(),Object(s)!==s))return}finally{if(u)throw n}}return o}}function Jv(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
|
||
|
|
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function eh(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
|
||
|
|
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function at(t,e){return jv(t)||Qv(t,e)||Uo(t,e)||Jv()}function Gn(t){return Yv(t)||Zv(t)||Uo(t)||eh()}function th(t,e){if(typeof t!="object"||!t)return t;var r=t[Symbol.toPrimitive];if(r!==void 0){var a=r.call(t,e);if(typeof a!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}function vd(t){var e=th(t,"string");return typeof e=="symbol"?e:e+""}function ot(t){"@babel/helpers - typeof";return ot=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ot(t)}function Uo(t,e){if(t){if(typeof t=="string")return ho(t,e);var r={}.toString.call(t).slice(8,-1);return r==="Object"&&t.constructor&&(r=t.constructor.name),r==="Map"||r==="Set"?Array.from(t):r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?ho(t,e):void 0}}var it=typeof window>"u"?null:window,Dl=it?it.navigator:null;it&&it.document;var rh=ot(""),hd=ot({}),ah=ot(function(){}),nh=typeof HTMLElement>"u"?"undefined":ot(HTMLElement),Qa=function(e){return e&&e.instanceString&&Ze(e.instanceString)?e.instanceString():null},me=function(e){return e!=null&&ot(e)==rh},Ze=function(e){return e!=null&&ot(e)===ah},Ue=function(e){return!It(e)&&(Array.isArray?Array.isArray(e):e!=null&&e instanceof Array)},Ne=function(e){return e!=null&&ot(e)===hd&&!Ue(e)&&e.constructor===Object},ih=function(e){return e!=null&&ot(e)===hd},se=function(e){return e!=null&&ot(e)===ot(1)&&!isNaN(e)},sh=function(e){return se(e)&&Math.floor(e)===e},Un=function(e){if(nh!=="undefined")return e!=null&&e instanceof HTMLElement},It=function(e){return Ja(e)||pd(e)},Ja=function(e){return Qa(e)==="collection"&&e._private.single},pd=function(e){return Qa(e)==="collection"&&!e._private.single},Ko=function(e){return Qa(e)==="core"},gd=function(e){return Qa(e)==="stylesheet"},oh=function(e){return Qa(e)==="event"},Sr=function(e){return e==null?!0:!!(e===""||e.match(/^\s+$/))},lh=function(e){return typeof HTMLElement>"u"?!1:e instanceof HTMLElement},uh=function(e){return Ne(e)&&se(e.x1)&&se(e.x2)&&se(e.y1)&&se(e.y2)},ch=function(e){return ih(e)&&Ze(e.then)},dh=function(){return Dl&&Dl.userAgent.match(/msie|trident|edge/i)},pa=function(e,r){r||(r=function(){if(arguments.length===1)return arguments[0];if(arguments.length===0)return"undefined";for(var i=[],s=0;s<arguments.length;s++)i.push(arguments[s]);return i.join("$")});var a=function(){var i=this,s=arguments,o,l=r.apply(i,s),u=a.cache;return(o=u[l])||(o=u[l]=e.apply(i,s)),o};return a.cache={},a},Wo=pa(function(t){return t.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),oi=pa(function(t){return t.replace(/(-\w)/g,function(e){return e[1].toUpperCase()})}),yd=pa(function(t,e){return t+e[0].toUpperCase()+e.substring(1)},function(t,e){return t+"$"+e}),Rl=function(e){return Sr(e)?e:e.charAt(0).toUpperCase()+e.substring(1)},br=function(e,r){return e.slice(-1*r.length)===r},st="(?:[-+]?(?:(?:\\d+|\\d*\\.\\d+)(?:[Ee][+-]?\\d+)?))",fh="rgb[a]?\\(("+st+"[%]?)\\s*,\\s*("+st+"[%]?)\\s*,\\s*("+st+"[%]?)(?:\\s*,\\s*("+st+"))?\\)",vh="rgb[a]?\\((?:"+st+"[%]?)\\s*,\\s*(?:"+st+"[%]?)\\s*,\\s*(?:"+st+"[%]?)(?:\\s*,\\s*(?:"+st+"))?\\)",hh="hsl[a]?\\(("+st+")\\s*,\\s*("+st+"[%])\\s*,\\s*("+st+"[%])(?:\\s*,\\s*("+st+"))?\\)",ph="hsl[a]?\\((?:"+st+")\\s*,\\s*(?:"+st+"[%])\\s*,\\s*(?:"+st+"[%])(?:\\s*,\\s*(?:"+st+"))?\\)",gh="\\#[0-9a-fA-F]{3}",yh="\\#[0-9a-fA-F]{6}",md=function(e,r){return e<r?-1:e>r?1:0},mh=function(e,r){return-1*md(e,r)},xe=Object.assign!=null?Object.assign.bind(Object):function(t){for(var e=arguments,r=1;r<e.length;r++){var a=e[r];if(a!=null)for(var n=Object.keys(a),i=0;i<n.length;i++){var s=n[i];t[s]=a[s]}}return t},bh=function(e){if(!(!(e.length===4||e.length===7)||e[0]!=="#")){var r=e.length===4,a,n,i,s=16;return r?(a=parseInt(e[1]+e[1],s),n=parseInt(e[2]+e[2],s),i=parseInt(e[3]+e[3],s)):(a=parseInt(e[1]+e[2],s),n=parseInt(e[3]+e[4],s),i=parseInt(e[5]+e[6],s)),[
|
||
|
|
`),d=t.pstyle("text-max-width").pfValue,f=t.pstyle("text-overflow-wrap").value,h=f==="anywhere",v=[],y=/[\s\u200b]+|$/g,p=0;p<c.length;p++){var g=c[p],m=this.calculateLabelDimensions(t,g),b=m.width;if(h){var w=g.split("").join(u);g=w}if(b>d){var E=g.matchAll(y),C="",x=0,T=Lt(E),k;try{for(T.s();!(k=T.n()).done;){var P=k.value,B=P[0],D=g.substring(x,P.index);x=P.index+B.length;var R=C.length===0?D:C+D+B,A=this.calculateLabelDimensions(t,R),M=A.width;M<=d?C+=D+B:(C&&v.push(C),C=D+B)}}catch($){T.e($)}finally{T.f()}C.match(/^[\s\u200b]+$/)||v.push(C)}else v.push(g)}s("labelWrapCachedLines",v),n=s("labelWrapCachedText",v.join(`
|
||
|
|
`)),s("labelWrapKey",l)}else if(o==="ellipsis"){var _=t.pstyle("text-max-width").pfValue,L="",I="…",H=!1;if(this.calculateLabelDimensions(t,n).width<_)return n;for(var G=0;G<n.length;G++){var O=this.calculateLabelDimensions(t,L+n[G]+I).width;if(O>_)break;L+=n[G],G===n.length-1&&(H=!0)}return H||(L+=I),L}return n};tr.getLabelJustification=function(t){var e=t.pstyle("text-justification").strValue,r=t.pstyle("text-halign").strValue;if(e==="auto")if(t.isNode())switch(r){case"left":return"right";case"right":return"left";default:return"center"}else return"center";else return e};tr.calculateLabelDimensions=function(t,e){var r=this,a=r.cy.window(),n=a.document,i=0,s=t.pstyle("font-style").strValue,o=t.pstyle("font-size").pfValue,l=t.pstyle("font-family").strValue,u=t.pstyle("font-weight").strValue,c=this.labelCalcCanvas,d=this.labelCalcCanvasContext;if(!c){c=this.labelCalcCanvas=n.createElement("canvas"),d=this.labelCalcCanvasContext=c.getContext("2d");var f=c.style;f.position="absolute",f.left="-9999px",f.top="-9999px",f.zIndex="-1",f.visibility="hidden",f.pointerEvents="none"}d.font="".concat(s," ").concat(u," ").concat(o,"px ").concat(l);for(var h=0,v=0,y=e.split(`
|
||
|
|
`),p=0;p<y.length;p++){var g=y[p],m=d.measureText(g),b=Math.ceil(m.width),w=o;h=Math.max(b,h),v+=w}return h+=i,v+=i,{width:h,height:v}};tr.calculateLabelAngle=function(t,e){var r=t._private,a=r.rscratch,n=t.isEdge(),i=e?e+"-":"",s=t.pstyle(i+"text-rotation"),o=s.strValue;return o==="none"?0:n&&o==="autorotate"?a.labelAutoAngle:o==="autorotate"?0:s.pfValue};tr.calculateLabelAngles=function(t){var e=this,r=t.isEdge(),a=t._private,n=a.rscratch;n.labelAngle=e.calculateLabelAngle(t),r&&(n.sourceLabelAngle=e.calculateLabelAngle(t,"source"),n.targetLabelAngle=e.calculateLabelAngle(t,"target"))};var _f={},zc=28,$c=!1;_f.getNodeShape=function(t){var e=this,r=t.pstyle("shape").value;if(r==="cutrectangle"&&(t.width()<zc||t.height()<zc))return $c||(He("The `cutrectangle` node shape can not be used at small sizes so `rectangle` is used instead"),$c=!0),"rectangle";if(t.isParent())return r==="rectangle"||r==="roundrectangle"||r==="round-rectangle"||r==="cutrectangle"||r==="cut-rectangle"||r==="barrel"?r:"rectangle";if(r==="polygon"){var a=t.pstyle("shape-polygon-points").value;return e.nodeShapes.makePolygon(a).name}return r};var Ci={};Ci.registerCalculationListeners=function(){var t=this.cy,e=t.collection(),r=this,a=function(s){var o=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;if(e.merge(s),o)for(var l=0;l<s.length;l++){var u=s[l],c=u._private,d=c.rstyle;d.clean=!1,d.cleanConnected=!1}};r.binder(t).on("bounds.* dirty.*",function(s){var o=s.target;a(o)}).on("style.* background.*",function(s){var o=s.target;a(o,!1)});var n=function(s){if(s){var o=r.onUpdateEleCalcsFns;e.cleanStyle();for(var l=0;l<e.length;l++){var u=e[l],c=u._private.rstyle;u.isNode()&&!c.cleanConnected&&(a(u.connectedEdges()),c.cleanConnected=!0)}if(o)for(var d=0;d<o.length;d++){var f=o[d];f(s,e)}r.recalculateRenderedStyle(e),e=t.collection()}};r.flushRenderedStyleQueue=function(){n(!0)},r.beforeRender(n,r.beforeRenderPriorities.eleCalcs)};Ci.onUpdateEleCalcs=function(t){var e=this.onUpdateEleCalcsFns=this.onUpdateEleCalcsFns||[];e.push(t)};Ci.recalculateRenderedStyle=function(t,e){var r=function(w){return w._private.rstyle.cleanConnected};if(t.length!==0){var a=[],n=[];if(!this.destroyed){e===void 0&&(e=!0);for(var i=0;i<t.length;i++){var s=t[i],o=s._private,l=o.rstyle;s.isEdge()&&(!r(s.source())||!r(s.target()))&&(l.clean=!1),s.isEdge()&&s.isBundledBezier()&&s.parallelEdges().some(function(b){return!b._private.rstyle.clean&&b.isBundledBezier()})&&(l.clean=!1),!(e&&l.clean||s.removed())&&s.pstyle("display").value!=="none"&&(o.group==="nodes"?n.push(s):a.push(s),l.clean=!0)}for(var u=0;u<n.length;u++){var c=n[u],d=c._private,f=d.rstyle,h=c.position();this.recalculateNodeLabelProjection(c),f.nodeX=h.x,f.nodeY=h.y,f.nodeW=c.pstyle("width").pfValue,f.nodeH=c.pstyle("height").pfValue}this.recalculateEdgeProjections(a);for(var v=0;v<a.length;v++){var y=a[v],p=y._private,g=p.rstyle,m=p.rscratch;g.srcX=m.arrowStartX,g.srcY=m.arrowStartY,g.tgtX=m.arrowEndX,g.tgtY=m.arrowEndY,g.midX=m.midX,g.midY=m.midY,g.labelAngle=m.labelAngle,g.sourceLabelAngle=m.sourceLabelAngle,g.targetLabelAngle=m.targetLabelAngle}}}};var Ti={};Ti.updateCachedGrabbedEles=function(){var t=this.cachedZSortedEles;if(t){t.drag=[],t.nondrag=[];for(var e=[],r=0;r<t.length;r++){var a=t[r],n=a._private.rscratch;a.grabbed()&&!a.isParent()?e.push(a):n.inDragLayer?t.drag.push(a):t.nondrag.push(a)}for(var r=0;r<e.length;r++){var a=e[r];t.drag.push(a)}}};Ti.invalidateCachedZSortedEles=function(){this.cachedZSortedEles=null};Ti.getCachedZSortedEles=function(t){if(t||!this.cachedZSortedEles){var e=this.cy.mutableElements().toArray();e.sort(pf),e.interactive=e.filter(function(r){return r.interactive()}),this.cachedZSortedEles=e,this.updateCachedGrabbedEles()}else e=this.cachedZSortedEles;return e};var If={};[Zr,ei,Tt,ln,vl,tr,_f,Ci,Ti].forEach(function(t){xe(If,t)});var Of={};Of.getCachedImage=function(t,e,r){var a=this,n=a.imageCache=a.imageCache||{},i=n[t];if(i)return i.image.complete||i.image.addEventListener("load",r),i.image;i=n[t]=n[t]||{};var s=i.image=new Image;s.addEventListener("load",r),s.addEv
|
||
|
|
float circleSD(vec2 p, float r) {
|
||
|
|
return distance(vec2(0), p) - r; // signed distance
|
||
|
|
}
|
||
|
|
`,nb=`
|
||
|
|
float rectangleSD(vec2 p, vec2 b) {
|
||
|
|
vec2 d = abs(p)-b;
|
||
|
|
return distance(vec2(0),max(d,0.0)) + min(max(d.x,d.y),0.0);
|
||
|
|
}
|
||
|
|
`,ib=`
|
||
|
|
float roundRectangleSD(vec2 p, vec2 b, vec4 cr) {
|
||
|
|
cr.xy = (p.x > 0.0) ? cr.xy : cr.zw;
|
||
|
|
cr.x = (p.y > 0.0) ? cr.x : cr.y;
|
||
|
|
vec2 q = abs(p) - b + cr.x;
|
||
|
|
return min(max(q.x, q.y), 0.0) + distance(vec2(0), max(q, 0.0)) - cr.x;
|
||
|
|
}
|
||
|
|
`,sb=`
|
||
|
|
float ellipseSD(vec2 p, vec2 ab) {
|
||
|
|
p = abs( p ); // symmetry
|
||
|
|
|
||
|
|
// find root with Newton solver
|
||
|
|
vec2 q = ab*(p-ab);
|
||
|
|
float w = (q.x<q.y)? 1.570796327 : 0.0;
|
||
|
|
for( int i=0; i<5; i++ ) {
|
||
|
|
vec2 cs = vec2(cos(w),sin(w));
|
||
|
|
vec2 u = ab*vec2( cs.x,cs.y);
|
||
|
|
vec2 v = ab*vec2(-cs.y,cs.x);
|
||
|
|
w = w + dot(p-u,v)/(dot(p-u,u)+dot(v,v));
|
||
|
|
}
|
||
|
|
|
||
|
|
// compute final point and distance
|
||
|
|
float d = length(p-ab*vec2(cos(w),sin(w)));
|
||
|
|
|
||
|
|
// return signed distance
|
||
|
|
return (dot(p/ab,p/ab)>1.0) ? d : -d;
|
||
|
|
}
|
||
|
|
`,Fa={SCREEN:{name:"screen",screen:!0},PICKING:{name:"picking",picking:!0}},ri={IGNORE:1,USE_BB:2},ro=0,Qc=1,Jc=2,ao=3,ia=4,Sn=5,Ma=6,_a=7,ob=(function(){function t(e,r,a){Ar(this,t),this.r=e,this.gl=r,this.maxInstances=a.webglBatchSize,this.atlasSize=a.webglTexSize,this.bgColor=a.bgColor,this.debug=a.webglDebug,this.batchDebugInfo=[],a.enableWrapping=!0,a.createTextureCanvas=$0,this.atlasManager=new tb(e,a),this.batchManager=new rb(a),this.simpleShapeOptions=new Map,this.program=this._createShaderProgram(Fa.SCREEN),this.pickingProgram=this._createShaderProgram(Fa.PICKING),this.vao=this._createVAO()}return Lr(t,[{key:"addAtlasCollection",value:function(r,a){this.atlasManager.addAtlasCollection(r,a)}},{key:"addTextureAtlasRenderType",value:function(r,a){this.atlasManager.addRenderType(r,a)}},{key:"addSimpleShapeRenderType",value:function(r,a){this.simpleShapeOptions.set(r,a)}},{key:"invalidate",value:function(r){var a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=a.type,i=this.atlasManager;return n?i.invalidate(r,{filterType:function(o){return o===n},forceRedraw:!0}):i.invalidate(r)}},{key:"gc",value:function(){this.atlasManager.gc()}},{key:"_createShaderProgram",value:function(r){var a=this.gl,n=`#version 300 es
|
||
|
|
precision highp float;
|
||
|
|
|
||
|
|
uniform mat3 uPanZoomMatrix;
|
||
|
|
uniform int uAtlasSize;
|
||
|
|
|
||
|
|
// instanced
|
||
|
|
in vec2 aPosition; // a vertex from the unit square
|
||
|
|
|
||
|
|
in mat3 aTransform; // used to transform verticies, eg into a bounding box
|
||
|
|
in int aVertType; // the type of thing we are rendering
|
||
|
|
|
||
|
|
// the z-index that is output when using picking mode
|
||
|
|
in vec4 aIndex;
|
||
|
|
|
||
|
|
// For textures
|
||
|
|
in int aAtlasId; // which shader unit/atlas to use
|
||
|
|
in vec4 aTex; // x/y/w/h of texture in atlas
|
||
|
|
|
||
|
|
// for edges
|
||
|
|
in vec4 aPointAPointB;
|
||
|
|
in vec4 aPointCPointD;
|
||
|
|
in vec2 aLineWidth; // also used for node border width
|
||
|
|
|
||
|
|
// simple shapes
|
||
|
|
in vec4 aCornerRadius; // for round-rectangle [top-right, bottom-right, top-left, bottom-left]
|
||
|
|
in vec4 aColor; // also used for edges
|
||
|
|
in vec4 aBorderColor; // aLineWidth is used for border width
|
||
|
|
|
||
|
|
// output values passed to the fragment shader
|
||
|
|
out vec2 vTexCoord;
|
||
|
|
out vec4 vColor;
|
||
|
|
out vec2 vPosition;
|
||
|
|
// flat values are not interpolated
|
||
|
|
flat out int vAtlasId;
|
||
|
|
flat out int vVertType;
|
||
|
|
flat out vec2 vTopRight;
|
||
|
|
flat out vec2 vBotLeft;
|
||
|
|
flat out vec4 vCornerRadius;
|
||
|
|
flat out vec4 vBorderColor;
|
||
|
|
flat out vec2 vBorderWidth;
|
||
|
|
flat out vec4 vIndex;
|
||
|
|
|
||
|
|
void main(void) {
|
||
|
|
int vid = gl_VertexID;
|
||
|
|
vec2 position = aPosition; // TODO make this a vec3, simplifies some code below
|
||
|
|
|
||
|
|
if(aVertType == `.concat(ro,`) {
|
||
|
|
float texX = aTex.x; // texture coordinates
|
||
|
|
float texY = aTex.y;
|
||
|
|
float texW = aTex.z;
|
||
|
|
float texH = aTex.w;
|
||
|
|
|
||
|
|
if(vid == 1 || vid == 2 || vid == 4) {
|
||
|
|
texX += texW;
|
||
|
|
}
|
||
|
|
if(vid == 2 || vid == 4 || vid == 5) {
|
||
|
|
texY += texH;
|
||
|
|
}
|
||
|
|
|
||
|
|
float d = float(uAtlasSize);
|
||
|
|
vTexCoord = vec2(texX / d, texY / d); // tex coords must be between 0 and 1
|
||
|
|
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0);
|
||
|
|
}
|
||
|
|
else if(aVertType == `).concat(ia," || aVertType == ").concat(_a,`
|
||
|
|
|| aVertType == `).concat(Sn," || aVertType == ").concat(Ma,`) { // simple shapes
|
||
|
|
|
||
|
|
// the bounding box is needed by the fragment shader
|
||
|
|
vBotLeft = (aTransform * vec3(0, 0, 1)).xy; // flat
|
||
|
|
vTopRight = (aTransform * vec3(1, 1, 1)).xy; // flat
|
||
|
|
vPosition = (aTransform * vec3(position, 1)).xy; // will be interpolated
|
||
|
|
|
||
|
|
// calculations are done in the fragment shader, just pass these along
|
||
|
|
vColor = aColor;
|
||
|
|
vCornerRadius = aCornerRadius;
|
||
|
|
vBorderColor = aBorderColor;
|
||
|
|
vBorderWidth = aLineWidth;
|
||
|
|
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0);
|
||
|
|
}
|
||
|
|
else if(aVertType == `).concat(Qc,`) {
|
||
|
|
vec2 source = aPointAPointB.xy;
|
||
|
|
vec2 target = aPointAPointB.zw;
|
||
|
|
|
||
|
|
// adjust the geometry so that the line is centered on the edge
|
||
|
|
position.y = position.y - 0.5;
|
||
|
|
|
||
|
|
// stretch the unit square into a long skinny rectangle
|
||
|
|
vec2 xBasis = target - source;
|
||
|
|
vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x));
|
||
|
|
vec2 point = source + xBasis * position.x + yBasis * aLineWidth[0] * position.y;
|
||
|
|
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * vec3(point, 1.0), 1.0);
|
||
|
|
vColor = aColor;
|
||
|
|
}
|
||
|
|
else if(aVertType == `).concat(Jc,`) {
|
||
|
|
vec2 pointA = aPointAPointB.xy;
|
||
|
|
vec2 pointB = aPointAPointB.zw;
|
||
|
|
vec2 pointC = aPointCPointD.xy;
|
||
|
|
vec2 pointD = aPointCPointD.zw;
|
||
|
|
|
||
|
|
// adjust the geometry so that the line is centered on the edge
|
||
|
|
position.y = position.y - 0.5;
|
||
|
|
|
||
|
|
vec2 p0, p1, p2, pos;
|
||
|
|
if(position.x == 0.0) { // The left side of the unit square
|
||
|
|
p0 = pointA;
|
||
|
|
p1 = pointB;
|
||
|
|
p2 = pointC;
|
||
|
|
pos = position;
|
||
|
|
} else { // The right side of the unit square, use same approach but flip the geometry upside down
|
||
|
|
p0 = pointD;
|
||
|
|
p1 = pointC;
|
||
|
|
p2 = pointB;
|
||
|
|
pos = vec2(0.0, -position.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
vec2 p01 = p1 - p0;
|
||
|
|
vec2 p12 = p2 - p1;
|
||
|
|
vec2 p21 = p1 - p2;
|
||
|
|
|
||
|
|
// Find the normal vector.
|
||
|
|
vec2 tangent = normalize(normalize(p12) + normalize(p01));
|
||
|
|
vec2 normal = vec2(-tangent.y, tangent.x);
|
||
|
|
|
||
|
|
// Find the vector perpendicular to p0 -> p1.
|
||
|
|
vec2 p01Norm = normalize(vec2(-p01.y, p01.x));
|
||
|
|
|
||
|
|
// Determine the bend direction.
|
||
|
|
float sigma = sign(dot(p01 + p21, normal));
|
||
|
|
float width = aLineWidth[0];
|
||
|
|
|
||
|
|
if(sign(pos.y) == -sigma) {
|
||
|
|
// This is an intersecting vertex. Adjust the position so that there's no overlap.
|
||
|
|
vec2 point = 0.5 * width * normal * -sigma / dot(normal, p01Norm);
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0);
|
||
|
|
} else {
|
||
|
|
// This is a non-intersecting vertex. Treat it like a mitre join.
|
||
|
|
vec2 point = 0.5 * width * normal * sigma * dot(normal, p01Norm);
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
vColor = aColor;
|
||
|
|
}
|
||
|
|
else if(aVertType == `).concat(ao,` && vid < 3) {
|
||
|
|
// massage the first triangle into an edge arrow
|
||
|
|
if(vid == 0)
|
||
|
|
position = vec2(-0.15, -0.3);
|
||
|
|
if(vid == 1)
|
||
|
|
position = vec2( 0.0, 0.0);
|
||
|
|
if(vid == 2)
|
||
|
|
position = vec2( 0.15, -0.3);
|
||
|
|
|
||
|
|
gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0);
|
||
|
|
vColor = aColor;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
gl_Position = vec4(2.0, 0.0, 0.0, 1.0); // discard vertex by putting it outside webgl clip space
|
||
|
|
}
|
||
|
|
|
||
|
|
vAtlasId = aAtlasId;
|
||
|
|
vVertType = aVertType;
|
||
|
|
vIndex = aIndex;
|
||
|
|
}
|
||
|
|
`),i=this.batchManager.getIndexArray(),s=`#version 300 es
|
||
|
|
precision highp float;
|
||
|
|
|
||
|
|
// declare texture unit for each texture atlas in the batch
|
||
|
|
`.concat(i.map(function(u){return"uniform sampler2D uTexture".concat(u,";")}).join(`
|
||
|
|
`),`
|
||
|
|
|
||
|
|
uniform vec4 uBGColor;
|
||
|
|
uniform float uZoom;
|
||
|
|
|
||
|
|
in vec2 vTexCoord;
|
||
|
|
in vec4 vColor;
|
||
|
|
in vec2 vPosition; // model coordinates
|
||
|
|
|
||
|
|
flat in int vAtlasId;
|
||
|
|
flat in vec4 vIndex;
|
||
|
|
flat in int vVertType;
|
||
|
|
flat in vec2 vTopRight;
|
||
|
|
flat in vec2 vBotLeft;
|
||
|
|
flat in vec4 vCornerRadius;
|
||
|
|
flat in vec4 vBorderColor;
|
||
|
|
flat in vec2 vBorderWidth;
|
||
|
|
|
||
|
|
out vec4 outColor;
|
||
|
|
|
||
|
|
`).concat(ab,`
|
||
|
|
`).concat(nb,`
|
||
|
|
`).concat(ib,`
|
||
|
|
`).concat(sb,`
|
||
|
|
|
||
|
|
vec4 blend(vec4 top, vec4 bot) { // blend colors with premultiplied alpha
|
||
|
|
return vec4(
|
||
|
|
top.rgb + (bot.rgb * (1.0 - top.a)),
|
||
|
|
top.a + (bot.a * (1.0 - top.a))
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
vec4 distInterp(vec4 cA, vec4 cB, float d) { // interpolate color using Signed Distance
|
||
|
|
// scale to the zoom level so that borders don't look blurry when zoomed in
|
||
|
|
// note 1.5 is an aribitrary value chosen because it looks good
|
||
|
|
return mix(cA, cB, 1.0 - smoothstep(0.0, 1.5 / uZoom, abs(d)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void main(void) {
|
||
|
|
if(vVertType == `).concat(ro,`) {
|
||
|
|
// look up the texel from the texture unit
|
||
|
|
`).concat(i.map(function(u){return"if(vAtlasId == ".concat(u,") outColor = texture(uTexture").concat(u,", vTexCoord);")}).join(`
|
||
|
|
else `),`
|
||
|
|
}
|
||
|
|
else if(vVertType == `).concat(ao,`) {
|
||
|
|
// mimics how canvas renderer uses context.globalCompositeOperation = 'destination-out';
|
||
|
|
outColor = blend(vColor, uBGColor);
|
||
|
|
outColor.a = 1.0; // make opaque, masks out line under arrow
|
||
|
|
}
|
||
|
|
else if(vVertType == `).concat(ia,` && vBorderWidth == vec2(0.0)) { // simple rectangle with no border
|
||
|
|
outColor = vColor; // unit square is already transformed to the rectangle, nothing else needs to be done
|
||
|
|
}
|
||
|
|
else if(vVertType == `).concat(ia," || vVertType == ").concat(_a,`
|
||
|
|
|| vVertType == `).concat(Sn," || vVertType == ").concat(Ma,`) { // use SDF
|
||
|
|
|
||
|
|
float outerBorder = vBorderWidth[0];
|
||
|
|
float innerBorder = vBorderWidth[1];
|
||
|
|
float borderPadding = outerBorder * 2.0;
|
||
|
|
float w = vTopRight.x - vBotLeft.x - borderPadding;
|
||
|
|
float h = vTopRight.y - vBotLeft.y - borderPadding;
|
||
|
|
vec2 b = vec2(w/2.0, h/2.0); // half width, half height
|
||
|
|
vec2 p = vPosition - vec2(vTopRight.x - b[0] - outerBorder, vTopRight.y - b[1] - outerBorder); // translate to center
|
||
|
|
|
||
|
|
float d; // signed distance
|
||
|
|
if(vVertType == `).concat(ia,`) {
|
||
|
|
d = rectangleSD(p, b);
|
||
|
|
} else if(vVertType == `).concat(_a,` && w == h) {
|
||
|
|
d = circleSD(p, b.x); // faster than ellipse
|
||
|
|
} else if(vVertType == `).concat(_a,`) {
|
||
|
|
d = ellipseSD(p, b);
|
||
|
|
} else {
|
||
|
|
d = roundRectangleSD(p, b, vCornerRadius.wzyx);
|
||
|
|
}
|
||
|
|
|
||
|
|
// use the distance to interpolate a color to smooth the edges of the shape, doesn't need multisampling
|
||
|
|
// we must smooth colors inwards, because we can't change pixels outside the shape's bounding box
|
||
|
|
if(d > 0.0) {
|
||
|
|
if(d > outerBorder) {
|
||
|
|
discard;
|
||
|
|
} else {
|
||
|
|
outColor = distInterp(vBorderColor, vec4(0), d - outerBorder);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if(d > innerBorder) {
|
||
|
|
vec4 outerColor = outerBorder == 0.0 ? vec4(0) : vBorderColor;
|
||
|
|
vec4 innerBorderColor = blend(vBorderColor, vColor);
|
||
|
|
outColor = distInterp(innerBorderColor, outerColor, d);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
vec4 outerColor;
|
||
|
|
if(innerBorder == 0.0 && outerBorder == 0.0) {
|
||
|
|
outerColor = vec4(0);
|
||
|
|
} else if(innerBorder == 0.0) {
|
||
|
|
outerColor = vBorderColor;
|
||
|
|
} else {
|
||
|
|
outerColor = blend(vBorderColor, vColor);
|
||
|
|
}
|
||
|
|
outColor = distInterp(vColor, outerColor, d - innerBorder);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
outColor = vColor;
|
||
|
|
}
|
||
|
|
|
||
|
|
`).concat(r.picking?`if(outColor.a == 0.0) discard;
|
||
|
|
else outColor = vIndex;`:"",`
|
||
|
|
}
|
||
|
|
`),o=z0(a,n,s);o.aPosition=a.getAttribLocation(o,"aPosition"),o.aIndex=a.getAttribLocation(o,"aIndex"),o.aVertType=a.getAttribLocation(o,"aVertType"),o.aTransform=a.getAttribLocation(o,"aTransform"),o.aAtlasId=a.getAttribLocation(o,"aAtlasId"),o.aTex=a.getAttribLocation(o,"aTex"),o.aPointAPointB=a.getAttribLocation(o,"aPointAPointB"),o.aPointCPointD=a.getAttribLocation(o,"aPointCPointD"),o.aLineWidth=a.getAttribLocation(o,"aLineWidth"),o.aColor=a.getAttribLocation(o,"aColor"),o.aCornerRadius=a.getAttribLocation(o,"aCornerRadius"),o.aBorderColor=a.getAttribLocation(o,"aBorderColor"),o.uPanZoomMatrix=a.getUniformLocation(o,"uPanZoomMatrix"),o.uAtlasSize=a.getUniformLocation(o,"uAtlasSize"),o.uBGColor=a.getUniformLocation(o,"uBGColor"),o.uZoom=a.getUniformLocation(o,"uZoom"),o.uTextures=[];for(var l=0;l<this.batchManager.getMaxAtlasesPerBatch();l++)o.uTextures.push(a.getUniformLocation(o,"uTexture".concat(l)));return o}},{key:"_createVAO",value:function(){var r=[0,0,1,0,1,1,0,0,1,1,0,1];this.vertexCount=r.length/2;var a=this.maxInstances,n=this.gl,i=this.program,s=n.createVertexArray();return n.bindVertexArray(s),W0(n,"vec2",i.aPosition,r),this.transformBuffer=j0(n,a,i.aTransform),this.indexBuffer=Xt(n,a,"vec4",i.aIndex),this.vertTypeBuffer=Xt(n,a,"int",i.aVertType),this.atlasIdBuffer=Xt(n,a,"int",i.aAtlasId),this.texBuffer=Xt(n,a,"vec4",i.aTex),this.pointAPointBBuffer=Xt(n,a,"vec4",i.aPointAPointB),this.pointCPointDBuffer=Xt(n,a,"vec4",i.aPointCPointD),this.lineWidthBuffer=Xt(n,a,"vec2",i.aLineWidth),this.colorBuffer=Xt(n,a,"vec4",i.aColor),this.cornerRadiusBuffer=Xt(n,a,"vec4",i.aCornerRadius),this.borderColorBuffer=Xt(n,a,"vec4",i.aBorderColor),n.bindVertexArray(null),s}},{key:"buffers",get:function(){var r=this;return this._buffers||(this._buffers=Object.keys(this).filter(function(a){return br(a,"Buffer")}).map(function(a){return r[a]})),this._buffers}},{key:"startFrame",value:function(r){var a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Fa.SCREEN;this.panZoomMatrix=r,this.renderTarget=a,this.batchDebugInfo=[],this.wrappedCount=0,this.simpleCount=0,this.startBatch()}},{key:"startBatch",value:function(){this.instanceCount=0,this.batchManager.startBatch()}},{key:"endFrame",value:function(){this.endBatch()}},{key:"_isVisible",value:function(r,a){return r.visible()?a&&a.isVisible?a.isVisible(r):!0:!1}},{key:"drawTexture",value:function(r,a,n){var i=this.atlasManager,s=this.batchManager,o=i.getRenderTypeOpts(n);if(this._isVisible(r,o)&&!(r.isEdge()&&!this._isValidEdge(r))){if(this.renderTarget.picking&&o.getTexPickingMode){var l=o.getTexPickingMode(r);if(l===ri.IGNORE)return;if(l==ri.USE_BB){this.drawPickingRectangle(r,a,n);return}}var u=i.getAtlasInfo(r,n),c=Lt(u),d;try{for(c.s();!(d=c.n()).done;){var f=d.value,h=f.atlas,v=f.tex1,y=f.tex2;s.canAddToCurrentBatch(h)||this.endBatch();for(var p=s.getAtlasIndexForBatch(h),g=0,m=[[v,!0],[y,!1]];g<m.length;g++){var b=at(m[g],2),w=b[0],E=b[1];if(w.w!=0){var C=this.instanceCount;this.vertTypeBuffer.getView(C)[0]=ro;var x=this.indexBuffer.getView(C);na(a,x);var T=this.atlasIdBuffer.getView(C);T[0]=p;var k=this.texBuffer.getView(C);k[0]=w.x,k[1]=w.y,k[2]=w.w,k[3]=w.h;var P=this.transformBuffer.getMatrixView(C);this.setTransformMatrix(r,P,o,f,E),this.instanceCount++,E||this.wrappedCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}}}}catch(B){c.e(B)}finally{c.f()}}}},{key:"setTransformMatrix",value:function(r,a,n,i){var s=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,o=0;if(n.shapeProps&&n.shapeProps.padding&&(o=r.pstyle(n.shapeProps.padding).pfValue),i){var l=i.bb,u=i.tex1,c=i.tex2,d=u.w/(u.w+c.w);s||(d=1-d);var f=this._getAdjustedBB(l,o,s,d);this._applyTransformMatrix(a,f,n,r)}else{var h=n.getBoundingBox(r),v=this._getAdjustedBB(h,o,!0,1);this._applyTransformMatrix(a,v,n,r)}}},{key:"_applyTransformMatrix",value:function(r,a,n,i){var s,o;Xc(r);var l=n.getRotation?n.getRotation(i):0;if(l!==0){var u=n.getRotationPoint(i),c=u.x,d=u.y;Vn(r,r,[c,d]),Zc(r,r,l);var f=n.getRotationOffset(i);s=f.x+(a.xOffset||0),o=f.y+(a.yOffset||0)}else s=a.x1,o=a.y1;Vn
|
||
|
|
<section id="graph" class="tab-panel">
|
||
|
|
<div class="graph-toolbar">
|
||
|
|
<input id="graphSearch" type="search" data-i18n-placeholder="graph.search_placeholder" />
|
||
|
|
<select id="graphEntityType"><option value="" data-i18n="graph.entity_type_all"></option></select>
|
||
|
|
<select id="graphPredicate"><option value="" data-i18n="graph.predicate_all"></option></select>
|
||
|
|
<label class="graph-slider">
|
||
|
|
<span data-i18n="graph.min_confidence"></span>
|
||
|
|
<input id="graphMinConfidence" type="range" min="0" max="1" step="0.05" value="0" />
|
||
|
|
<output id="graphMinConfidenceOut">0%</output>
|
||
|
|
</label>
|
||
|
|
<label class="graph-slider">
|
||
|
|
<span data-i18n="graph.layout"></span>
|
||
|
|
<select id="graphLayout">
|
||
|
|
<option value="cose" data-i18n="graph.layout.cose"></option>
|
||
|
|
<option value="concentric" data-i18n="graph.layout.concentric"></option>
|
||
|
|
<option value="circle" data-i18n="graph.layout.circle"></option>
|
||
|
|
<option value="breadthfirst" data-i18n="graph.layout.breadthfirst"></option>
|
||
|
|
<option value="grid" data-i18n="graph.layout.grid"></option>
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<button id="graphFitBtn" data-i18n="graph.fit"></button>
|
||
|
|
<button id="graphReloadBtn" class="primary" data-i18n="graph.reload"></button>
|
||
|
|
</div>
|
||
|
|
<div class="graph-stats">
|
||
|
|
<span id="graphStats" class="muted"></span>
|
||
|
|
<span id="graphLegend" class="graph-legend"></span>
|
||
|
|
</div>
|
||
|
|
<div class="graph-stage">
|
||
|
|
<div id="graphContainer"></div>
|
||
|
|
<div id="graphEmpty" class="graph-empty muted" data-i18n="graph.empty"></div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
`}function Pb(){xt(q("graph")),q("graphSearch").addEventListener("input",t=>{bt.search=t.target.value.trim().toLowerCase(),kn()}),q("graphEntityType").addEventListener("change",t=>{bt.entity_type=t.target.value,kn()}),q("graphPredicate").addEventListener("change",t=>{bt.predicate=t.target.value,kn()}),q("graphMinConfidence").addEventListener("input",t=>{bt.min_confidence=Number(t.target.value),q("graphMinConfidenceOut").textContent=`${Math.round(bt.min_confidence*100)}%`,kn()}),q("graphLayout").addEventListener("change",t=>{bt.layout=t.target.value,iv()}),q("graphFitBtn").addEventListener("click",()=>Qt?.fit(void 0,40)),q("graphReloadBtn").addEventListener("click",()=>gl().catch(()=>{})),fr(()=>xt(q("graph")))}async function gl(){if(!ue.selectedProject)return;lr=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/graph/neighborhood?limit=300`)??{nodes:[],edges:[]},Bb(lr.nodes),Db(lr),_o()}function Bb(t){const e=[...new Set(t.map(r=>r.type).filter(Boolean))].sort();ni=new Map,e.forEach((r,a)=>ni.set(r,ad[a%ad.length]))}function Db(t){const e=q("graphEntityType"),r=q("graphPredicate"),a=[...new Set(t.nodes.map(i=>i.type).filter(Boolean))].sort(),n=[...new Set(t.edges.map(i=>i.predicate).filter(Boolean))].sort();e.innerHTML=`<option value="">${z(V("graph.entity_type_all"))}</option>`+a.map(i=>`<option value="${z(i)}" ${i===bt.entity_type?"selected":""}>${z(i)}</option>`).join(""),r.innerHTML=`<option value="">${z(V("graph.predicate_all"))}</option>`+n.map(i=>`<option value="${z(i)}" ${i===bt.predicate?"selected":""}>${z(i)}</option>`).join(""),Rb(a)}function Rb(t){q("graphLegend").innerHTML=t.slice(0,12).map(e=>`
|
||
|
|
<span class="legend-item"><i style="background:${ni.get(e)}"></i>${z(e)}</span>
|
||
|
|
`).join("")}function Ab(){const t=bt.min_confidence,e=bt.search,r=bt.entity_type,a=bt.predicate,n=new Set,i=[];for(const l of lr.edges)l.confidence<t||a&&l.predicate!==a||l.target!=null&&(n.add(l.source),n.add(l.target));const s=lr.nodes.filter(l=>r&&l.type!==r||e&&!(l.name||"").toLowerCase().includes(e)&&!(l.canonical_name||"").toLowerCase().includes(e)?!1:bt.predicate||bt.min_confidence>0?n.has(l.id):!0),o=new Set(s.map(l=>l.id));return s.forEach(l=>{i.push({group:"nodes",data:{id:`n${l.id}`,entityId:l.id,label:l.name,type:l.type,canonical:l.canonical_name,metadata:l.metadata,color:ni.get(l.type)||"#7aa2ff"}})}),lr.edges.forEach(l=>{l.confidence<t||a&&l.predicate!==a||l.target!=null&&(!o.has(l.source)||!o.has(l.target)||i.push({group:"edges",data:{id:`c${l.claim_id}`,claimId:l.claim_id,source:`n${l.source}`,target:`n${l.target}`,predicate:l.predicate,confidence:l.confidence,metadata:l.metadata,targetName:l.object?.name}}))}),{elements:i,visibleNodeCount:s.length}}function _o(){const{elements:t,visibleNodeCount:e}=Ab();Lb(e);const r=q("graphEmpty");lr.nodes.length?r.style.display="none":r.style.display="flex",Qt?(Qt.elements().remove(),Qt.add(t),iv()):(Qt=Xr({container:q("graphContainer"),elements:t,wheelSensitivity:.2,style:Mb(),layout:sv(bt.layout)}),Qt.on("tap","node",a=>{const n=a.target.data();ue.selection={kind:"graph_node",data:n},ur()}),Qt.on("tap","edge",a=>{const n=a.target.data();ue.selection={kind:"graph_edge",data:n},ur()}))}function kn(){if(!Qt)return _o();_o()}function iv(){Qt&&Qt.layout(sv(bt.layout)).run()}function sv(t){return t==="cose"?{name:"cose",animate:!1,idealEdgeLength:110,nodeRepulsion:16e3,padding:40}:t==="concentric"?{name:"concentric",concentric:e=>e.degree(!1),levelWidth:()=>1,animate:!1,padding:30}:t==="breadthfirst"?{name:"breadthfirst",directed:!0,animate:!1,padding:30,spacingFactor:1.2}:{name:t,animate:!1,padding:30}}function Lb(t){const e=lr.nodes.length,r=lr.edges.length;q("graphStats").textContent=`${e} ${V("graph.stats.nodes")} · ${r} ${V("graph.stats.edges")} · ${t} ${V("graph.stats.shown")}`}function Mb(){return[{selector:"node",style:{"background-color":"data(color)",label:"data(label)",color:"#e6ecf3","font-size":11,"text-outline-color":"#0f1216","text-outline-width":2,"text-valign":"bottom","text-margin-y":6,width:24,height:24,"border-width":1,"border-color":"#0f1216"}},{selector:"node:selected",style:{"border-color":"#ffffff","border-width":2,width:28,height:28}},{selector:"edge",style:{width:"mapData(confidence, 0, 1, 1, 4)","line-color":"#364154","target-arrow-color":"#364154","target-arrow-shape":"triangle","curve-style":"bezier",label:"data(predicate)","font-size":9,color:"#8c98ab","text-rotation":"autorotate","text-background-color":"#0f1216","text-background-opacity":.85,"text-background-padding":2,"arrow-scale":.8}},{selector:"edge:selected",style:{"line-color":"#7aa2ff","target-arrow-color":"#7aa2ff",color:"#7aa2ff",width:3}}]}let Si=[],_t=[],Ot=null;const wt=new Set,sr={status:"",predicate:"",search:""};let Pn=null;function _b(){return`
|
||
|
|
<section id="claims" class="tab-panel">
|
||
|
|
<div class="workbench">
|
||
|
|
<div class="workbench-toolbar">
|
||
|
|
<select id="wbStatusFilter">
|
||
|
|
<option value="" data-i18n="workbench.status.all"></option>
|
||
|
|
<option value="active" data-i18n="workbench.status.active"></option>
|
||
|
|
<option value="validated_claim" data-i18n="workbench.status.validated_claim"></option>
|
||
|
|
<option value="rejected" data-i18n="workbench.status.rejected"></option>
|
||
|
|
</select>
|
||
|
|
<select id="wbPredicateFilter">
|
||
|
|
<option value="" data-i18n="workbench.predicate_all"></option>
|
||
|
|
</select>
|
||
|
|
<input id="wbSearch" type="search" data-i18n-placeholder="workbench.search_placeholder" />
|
||
|
|
<span class="wb-selected" id="wbSelectedCount"></span>
|
||
|
|
<button id="wbBulkAcceptBtn" class="wb-success" disabled data-i18n="workbench.action.accept"></button>
|
||
|
|
<button id="wbBulkRejectBtn" class="wb-danger" disabled data-i18n="workbench.action.reject"></button>
|
||
|
|
</div>
|
||
|
|
<div class="workbench-body">
|
||
|
|
<div class="workbench-list" id="wbList"></div>
|
||
|
|
<div class="workbench-detail" id="wbDetail"></div>
|
||
|
|
</div>
|
||
|
|
<div class="workbench-shortcuts">
|
||
|
|
<strong data-i18n="workbench.shortcuts"></strong>
|
||
|
|
<span data-i18n="workbench.shortcut.nav"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.accept"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.reject"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.toggle"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.bulk_accept"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.bulk_reject"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.search"></span>
|
||
|
|
<span data-i18n="workbench.shortcut.escape"></span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
`}function Ib(){const t=q("claims");xt(t),fr(()=>{xt(t),Er()}),q("wbStatusFilter").addEventListener("change",e=>{sr.status=e.target.value,Er()}),q("wbPredicateFilter").addEventListener("change",e=>{sr.predicate=e.target.value,Er()}),q("wbSearch").addEventListener("input",e=>{sr.search=e.target.value.trim().toLowerCase(),Er()}),q("wbBulkAcceptBtn").addEventListener("click",()=>ii("validated_claim")),q("wbBulkRejectBtn").addEventListener("click",()=>ii("rejected")),Hb()}async function Ta(){if(!ue.selectedProject)return;const t=encodeURIComponent(ue.selectedProject),[e,r]=await Promise.all([Fe(`/projects/${t}/claims?limit=200&include_candidates=true`),Fe(`/projects/${t}/claims?limit=200&status=rejected`).catch(()=>[])]),a=new Map;for(const n of[...e,...r])a.set(n.id,n);Si=[...a.values()].sort((n,i)=>(i.last_seen_at??"").localeCompare(n.last_seen_at??"")),Ob(),Er()}function Ob(){const t=q("wbPredicateFilter");if(!t)return;const e=[...new Set(Si.map(a=>a.predicate).filter(Boolean))].sort(),r=sr.predicate;t.innerHTML=`<option value="">${z(V("workbench.predicate_all"))}</option>`+e.map(a=>`<option value="${z(a)}" ${a===r?"selected":""}>${z(a)}</option>`).join("")}function Nb(){const t=sr.search;_t=Si.filter(e=>!(sr.status&&e.status!==sr.status||sr.predicate&&e.predicate!==sr.predicate||t&&!`${e.subject??""} ${e.predicate??""} ${e.object??""} ${e.evidence_text??""}`.toLowerCase().includes(t))),_t.some(e=>e.id===Ot)||(Ot=_t[0]?.id??null);for(const e of[...wt])_t.some(r=>r.id===e)||wt.delete(e)}function Er(){Nb(),ov(),lv(),uv()}function ov(){const t=q("wbList");if(!_t.length){t.innerHTML=`<div class="wb-empty muted" data-i18n="workbench.empty">${z(V("workbench.empty"))}</div>`;return}t.innerHTML=_t.map(e=>zb(e)).join(""),t.querySelectorAll("[data-claim-row]").forEach(e=>{const r=Number(e.dataset.claimRow);e.addEventListener("click",a=>{a.shiftKey||a.ctrlKey||a.metaKey?cv(r):(Ot=r,wt.clear(),wt.add(r)),ov(),lv(),uv(),nd()})}),nd()}function zb(t){const e=t.id===Ot,r=wt.has(t.id),a=`workbench.status.${t.status??"active"}`;return`
|
||
|
|
<button class="wb-row ${e?"active":""} ${r?"selected":""}"
|
||
|
|
data-claim-row="${t.id}" data-status="${z(t.status??"active")}">
|
||
|
|
<span class="wb-row-main">
|
||
|
|
<strong>${z(t.subject??"")}</strong>
|
||
|
|
<em class="predicate">${z(t.predicate??"")}</em>
|
||
|
|
<span>${z(t.object??"")}</span>
|
||
|
|
</span>
|
||
|
|
<span class="wb-row-meta">
|
||
|
|
<span class="wb-status" data-status="${z(t.status??"active")}">${z(V(a,t.status??""))}</span>
|
||
|
|
<span class="wb-confidence">${Math.round((t.confidence??0)*100)}%</span>
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
`}function lv(){const t=q("wbDetail"),e=_t.find(n=>n.id===Ot);if(!e){t.innerHTML=`<div class="wb-empty muted">${z(V("workbench.select_hint"))}</div>`;return}const r=`workbench.status.${e.status??"active"}`,a=e.confidence_breakdown?`LLM ${Bn(e.confidence_breakdown.llm_confidence)} · evidence ${Bn(e.confidence_breakdown.evidence_confidence)} · ontology ${Bn(e.confidence_breakdown.ontology_confidence)} · stored ${Bn(e.confidence_breakdown.stored_confidence??e.confidence)}`:"";t.innerHTML=`
|
||
|
|
<div class="wb-detail-head">
|
||
|
|
<div>
|
||
|
|
<h3>${z(e.subject??"")} <span class="predicate">${z(e.predicate??"")}</span> ${z(e.object??"")}</h3>
|
||
|
|
<div class="wb-detail-status">
|
||
|
|
<span class="wb-status" data-status="${z(e.status??"active")}">${z(V(r,e.status??""))}</span>
|
||
|
|
<span class="wb-confidence">${Math.round((e.confidence??0)*100)}%</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="wb-actions">
|
||
|
|
<button class="wb-success" data-action="accept">${z(V("workbench.action.accept"))} <kbd>A</kbd></button>
|
||
|
|
<button class="wb-danger" data-action="reject">${z(V("workbench.action.reject"))} <kbd>R</kbd></button>
|
||
|
|
<button data-action="unreview">${z(V("workbench.action.unreview"))}</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<dl class="wb-kv">
|
||
|
|
<dt>${z(V("workbench.detail.subject"))}</dt><dd>${z(e.subject??"")} <span class="muted">(${z(e.subject_type??"")})</span></dd>
|
||
|
|
<dt>${z(V("workbench.detail.predicate"))}</dt><dd>${z(e.predicate??"")}</dd>
|
||
|
|
<dt>${z(V("workbench.detail.object"))}</dt><dd>${z(e.object??JSON.stringify(e.object_value??""))}</dd>
|
||
|
|
<dt>${z(V("workbench.detail.source"))}</dt><dd>${z(e.source??"")}</dd>
|
||
|
|
<dt>${z(V("workbench.detail.page"))}</dt><dd>${e.page_url?`<a href="${z(e.page_url)}" target="_blank" rel="noreferrer">${z(e.page_url)}</a>`:"-"}</dd>
|
||
|
|
${a?`<dt>${z(V("workbench.detail.breakdown"))}</dt><dd>${z(a)}</dd>`:""}
|
||
|
|
</dl>
|
||
|
|
|
||
|
|
${e.evidence_text?`
|
||
|
|
<section class="wb-evidence">
|
||
|
|
<h4>${z(V("workbench.detail.evidence"))}</h4>
|
||
|
|
<pre>${$b(e.evidence_text,e)}</pre>
|
||
|
|
</section>
|
||
|
|
`:""}
|
||
|
|
|
||
|
|
<div class="wb-confidence-edit">
|
||
|
|
<label>${z(V("workbench.detail.confidence"))}
|
||
|
|
<input id="wbConfidence" type="number" min="0" max="1" step="0.01" value="${e.confidence??0}" />
|
||
|
|
</label>
|
||
|
|
<input id="wbReason" data-i18n-placeholder="workbench.detail.reason_placeholder" value="${z(e.confidence_reason??"")}" />
|
||
|
|
<button data-action="save-confidence">${z(V("workbench.action.save_confidence"))}</button>
|
||
|
|
<button data-action="accept-similar">${z(V("workbench.action.accept_similar"))}</button>
|
||
|
|
</div>
|
||
|
|
`,xt(t),t.querySelectorAll("[data-action]").forEach(n=>{n.addEventListener("click",()=>Io(n.dataset.action))})}function $b(t,e){const r=[];e.subject&&r.push(e.subject),e.object&&r.push(e.object);let a=z(t);for(const n of r){if(!n||n.length<2)continue;const i=Fb(n);a=a.replace(new RegExp(i,"gi"),s=>`<mark>${s}</mark>`)}return a}function Fb(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function uv(){const t=wt.size;q("wbSelectedCount").textContent=t>1?V("workbench.bulk_count").replace("{count}",t):"";const e=t>1;q("wbBulkAcceptBtn").disabled=!e,q("wbBulkRejectBtn").disabled=!e}function Bn(t){return t==null?"-":`${Math.round(t*100)}%`}function cv(t){wt.has(t)?wt.delete(t):wt.add(t),wt.size===1&&(Ot=[...wt][0])}function nd(){const t=document.querySelector(`[data-claim-row="${Ot}"]`);t&&t.scrollIntoView({block:"nearest"})}async function Io(t){const e=_t.find(r=>r.id===Ot);e&&(t==="accept"?await oo(e.id,"validated_claim"):t==="reject"?await oo(e.id,"rejected"):t==="unreview"?await oo(e.id,"active"):t==="save-confidence"?await qb(e):t==="accept-similar"&&await Vb(e))}async function oo(t,e){const r=q("wbReason")?.value?.trim()||null;try{await Fe(`/claims/${t}/status`,{method:"PATCH",body:JSON.stringify({status:e,reason:r})}),Ae(V("workbench.toast.reviewed")),await Ta()}catch(a){Ae(a.message)}}async function qb(t){const e=Number(q("wbConfidence").value),r=q("wbReason").value.trim()||null;try{await Fe(`/claims/${t.id}/confidence`,{method:"PATCH",body:JSON.stringify({confidence:e,reason:r})}),Ae(V("toast.claim_updated")),await Ta()}catch(a){Ae(a.message)}}async function Vb(t){const e=t.object??JSON.stringify(t.object_value??""),r=Si.filter(a=>a.predicate===t.predicate&&(a.object??JSON.stringify(a.object_value??""))===e).map(a=>a.id);if(r.length)try{const a=await Fe("/claims/bulk-status",{method:"POST",body:JSON.stringify({claim_ids:r,status:"validated_claim",reason:q("wbReason")?.value?.trim()||null})});Ae(V("workbench.toast.bulk_done").replace("{count}",a.updated??r.length)),await Ta()}catch(a){Ae(a.message)}}async function ii(t){const e=[...wt];if(!(e.length<2))try{const r=await Fe("/claims/bulk-status",{method:"POST",body:JSON.stringify({claim_ids:e,status:t,reason:null})});Ae(V("workbench.toast.bulk_done").replace("{count}",r.updated??e.length)),wt.clear(),await Ta()}catch(r){Ae(r.message)}}function Hb(){Pn&&document.removeEventListener("keydown",Pn),Pn=t=>{if(!Gb()||t.target.matches("input, textarea, select"))return;const e=t.key;if(e==="j"||e==="ArrowDown"){t.preventDefault(),id(1);return}if(e==="k"||e==="ArrowUp"){t.preventDefault(),id(-1);return}if(e===" "){t.preventDefault(),Ot!=null&&(cv(Ot),Er());return}if(e==="Escape"){wt.clear(),Er();return}if(e==="/"){t.preventDefault(),q("wbSearch")?.focus();return}if(e==="a"&&!t.shiftKey){t.preventDefault(),Io("accept");return}if(e==="r"&&!t.shiftKey){t.preventDefault(),Io("reject");return}if(t.shiftKey&&(e==="A"||e==="a")){t.preventDefault(),ii("validated_claim");return}if(t.shiftKey&&(e==="R"||e==="r")){t.preventDefault(),ii("rejected");return}},document.addEventListener("keydown",Pn)}function Gb(){const t=q("claims");return t&&t.classList.contains("active")}function id(t){if(!_t.length)return;let r=_t.findIndex(a=>a.id===Ot)+t;r<0&&(r=0),r>=_t.length&&(r=_t.length-1),Ot=_t[r].id,wt.clear(),wt.add(Ot),Er()}let qr=null;function Ub(){return`
|
||
|
|
<section id="overview" class="tab-panel active">
|
||
|
|
<div class="pipeline-head">
|
||
|
|
<h2 data-i18n="pipeline.heading"></h2>
|
||
|
|
<button id="pipelineRefreshBtn" class="ghost" data-i18n="pipeline.refresh"></button>
|
||
|
|
</div>
|
||
|
|
<div class="pipeline-stages" id="pipelineStages"></div>
|
||
|
|
|
||
|
|
<div class="summary-grid" id="pipelineSummary"></div>
|
||
|
|
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="pipeline.entity_types_heading"></h2>
|
||
|
|
<div id="pipelineEntityTypes" class="entity-type-bars"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="split">
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="pipeline.recent_pages"></h2>
|
||
|
|
<div id="pipelineRecentPages" class="recent-list"></div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="pipeline.recent_claims"></h2>
|
||
|
|
<div id="pipelineRecentClaims" class="recent-list"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="overview.sources_heading"></h2>
|
||
|
|
<div id="sourceTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
`}function Kb(){const t=q("overview");xt(t),q("pipelineRefreshBtn").addEventListener("click",()=>yl().catch(()=>{})),fr(()=>{xt(t),dv()})}async function yl(){ue.selectedProject&&(qr=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/pipeline`),dv())}function dv(){qr&&(jb(qr.stages||[]),Xb(qr.stages||[]),Zb(qr.entity_types||[]),Qb(qr.recent_pages||[]),Jb(qr.recent_claims||[]))}const Wb={crawled:"#7aa2ff",extracted:"#22d3ee",claims:"#facc15",validated:"#4ade80",graph:"#a78bfa"};function jb(t){const e=Math.max(1,...t.map(a=>a.count||0)),r=q("pipelineStages");r.innerHTML=t.map((a,n)=>{const i=n>0?t[n-1].count:null,s=i&&i>0?Math.round(a.count/i*100):null,o=a.count/e*100,l=Wb[a.key]||"#7aa2ff",u=Yb(a);return`
|
||
|
|
<div class="pipeline-card" style="--stage-color:${l}">
|
||
|
|
<div class="pipeline-card-head">
|
||
|
|
<span class="pipeline-stage-name">${z(V(`pipeline.stage.${a.key}`,a.key))}</span>
|
||
|
|
${s!=null?`<span class="pipeline-retention">${s}% ${z(V("pipeline.retained"))}</span>`:""}
|
||
|
|
</div>
|
||
|
|
<div class="pipeline-count">${a.count}</div>
|
||
|
|
<div class="pipeline-bar"><div class="pipeline-bar-fill" style="width:${o}%"></div></div>
|
||
|
|
${u?`<div class="pipeline-extra muted">${u}</div>`:""}
|
||
|
|
</div>
|
||
|
|
`}).join("")}function Yb(t){return t.extra?t.key==="extracted"?`events ${t.extra.events??0} · errors ${t.extra.errors??0}`:t.key==="claims"?Object.entries(t.extra).map(([e,r])=>`${z(e)} ${r}`).join(" · "):t.key==="graph"?`entities ${t.extra.entities??0}`:"":""}function Xb(t){const e=ue.projectDetail,r=t.find(n=>n.key==="claims"),a=t.find(n=>n.key==="validated");q("pipelineSummary").innerHTML=`
|
||
|
|
<div class="metric"><span>${z(V("overview.project"))}</span><strong>${z(e?.name??"-")}</strong></div>
|
||
|
|
<div class="metric"><span>${z(V("overview.domain"))}</span><strong>${z(e?.domain??"-")}</strong></div>
|
||
|
|
<div class="metric"><span>${z(V("overview.sources"))}</span><strong>${(e?.sources??[]).length}</strong></div>
|
||
|
|
<div class="metric"><span>${z(V("overview.entities"))}</span><strong id="metricEntities">${t.find(n=>n.key==="graph")?.extra?.entities??0}</strong></div>
|
||
|
|
<div class="metric"><span>${z(V("pipeline.stage.claims"))}</span><strong>${r?.count??0}</strong></div>
|
||
|
|
<div class="metric"><span>${z(V("pipeline.stage.validated"))}</span><strong>${a?.count??0}</strong></div>
|
||
|
|
`}function Zb(t){const e=q("pipelineEntityTypes");if(!t.length){e.innerHTML=`<p class="muted" style="font-size:11px;margin:0">${z(V("pipeline.empty.claims"))}</p>`;return}const r=Math.max(1,...t.map(a=>a.count));e.innerHTML=t.map(a=>{const n=a.count/r*100;return`
|
||
|
|
<div class="entity-type-row">
|
||
|
|
<span class="entity-type-name">${z(a.type)}</span>
|
||
|
|
<div class="entity-type-bar"><div class="entity-type-bar-fill" style="width:${n}%"></div></div>
|
||
|
|
<span class="entity-type-count">${a.count}</span>
|
||
|
|
</div>
|
||
|
|
`}).join("")}function Qb(t){const e=q("pipelineRecentPages");if(!t.length){e.innerHTML=`<p class="muted" style="font-size:11px;margin:0">${z(V("pipeline.empty.pages"))}</p>`;return}e.innerHTML=t.map(r=>`
|
||
|
|
<div class="recent-item">
|
||
|
|
<div class="recent-line">
|
||
|
|
<span class="recent-badge">${z(r.page_type??"")}</span>
|
||
|
|
<span class="recent-code">${z(r.status_code??"-")}</span>
|
||
|
|
<span class="recent-time">${z(fv(r.fetched_at))}</span>
|
||
|
|
</div>
|
||
|
|
<div class="recent-title">${z(r.title??r.url??"")}</div>
|
||
|
|
<a class="recent-url" href="${z(r.url??"")}" target="_blank" rel="noreferrer">${z(r.url??"")}</a>
|
||
|
|
</div>
|
||
|
|
`).join("")}function Jb(t){const e=q("pipelineRecentClaims");if(!t.length){e.innerHTML=`<p class="muted" style="font-size:11px;margin:0">${z(V("pipeline.empty.claims"))}</p>`;return}e.innerHTML=t.map(r=>`
|
||
|
|
<div class="recent-item">
|
||
|
|
<div class="recent-line">
|
||
|
|
<strong>${z(r.subject??"")}</strong>
|
||
|
|
<span class="recent-predicate">${z(r.predicate??"")}</span>
|
||
|
|
</div>
|
||
|
|
<div class="recent-line">
|
||
|
|
<span class="wb-status" data-status="${z(r.status??"active")}">${z(r.status??"")}</span>
|
||
|
|
<span class="recent-conf">${Math.round((r.confidence??0)*100)}%</span>
|
||
|
|
<span class="recent-time">${z(fv(r.last_seen_at))}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`).join("")}function fv(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return"";const a=(Date.now()-e.getTime())/1e3;return a<60?`${Math.floor(a)}s`:a<3600?`${Math.floor(a/60)}m`:a<86400?`${Math.floor(a/3600)}h`:`${Math.floor(a/86400)}d`}function ew(t){const e=q("sourceTable");if(!e)return;const r=t?.sources??[];if(!r.length){e.innerHTML=`<table><tbody><tr><td class="muted">${z(V("table.no_data"))}</td></tr></tbody></table>`;return}e.innerHTML=`
|
||
|
|
<table>
|
||
|
|
<thead><tr>
|
||
|
|
<th>${z(V("table.name"))}</th>
|
||
|
|
<th>${z(V("table.type"))}</th>
|
||
|
|
<th>${z(V("table.trust"))}</th>
|
||
|
|
<th>${z(V("table.robots"))}</th>
|
||
|
|
<th>${z(V("table.rate"))}</th>
|
||
|
|
</tr></thead>
|
||
|
|
<tbody>${r.map(a=>`
|
||
|
|
<tr>
|
||
|
|
<td>${z(a.name)}</td>
|
||
|
|
<td>${z(a.type)}</td>
|
||
|
|
<td>${a.trust_level}</td>
|
||
|
|
<td>${a.respect_robots_txt?"on":"off"}</td>
|
||
|
|
<td>${a.rate_limit_per_minute}/min</td>
|
||
|
|
</tr>
|
||
|
|
`).join("")}</tbody>
|
||
|
|
</table>
|
||
|
|
`}let Dn=null;function tw(t){Dn=t,Dn.innerHTML=rw(),xt(Dn),document.querySelectorAll(".tab").forEach(e=>{e.addEventListener("click",()=>{document.querySelectorAll(".tab").forEach(r=>r.classList.remove("active")),document.querySelectorAll(".tab-panel").forEach(r=>r.classList.remove("active")),e.classList.add("active"),document.getElementById(e.dataset.tab).classList.add("active")})}),Pb(),Ib(),Kb(),q("loadOntologyRegistryBtn").addEventListener("click",Oo),q("loadEntitiesBtn").addEventListener("click",No),q("mergeEntitiesBtn").addEventListener("click",aw),q("loadExtractionLogsBtn").addEventListener("click",zo),q("runResearchBtn").addEventListener("click",iw),q("loadResearchBtn").addEventListener("click",$o),q("loadGraphQueryBtn").addEventListener("click",Fo),q("loadTagsBtn").addEventListener("click",qo),q("recommendBtn").addEventListener("click",lw),fr(()=>{xt(Dn),vv(),hv(),yl().catch(()=>{}),Oo().catch(()=>{}),No().catch(()=>{}),Ta().catch(()=>{}),zo().catch(()=>{}),$o().catch(()=>{}),Fo().catch(()=>{}),qo().catch(()=>{}),gl().catch(()=>{})})}function rw(){return`
|
||
|
|
<nav class="tabs" aria-label="Admin sections">
|
||
|
|
<button class="tab active" data-tab="overview" data-i18n="tabs.overview"></button>
|
||
|
|
<button class="tab" data-tab="graph" data-i18n="tabs.graph"></button>
|
||
|
|
<button class="tab" data-tab="ontology" data-i18n="tabs.ontology"></button>
|
||
|
|
<button class="tab" data-tab="entities" data-i18n="tabs.entities"></button>
|
||
|
|
<button class="tab" data-tab="claims" data-i18n="tabs.claims"></button>
|
||
|
|
<button class="tab" data-tab="debug" data-i18n="tabs.debug"></button>
|
||
|
|
<button class="tab" data-tab="research" data-i18n="tabs.research"></button>
|
||
|
|
<button class="tab" data-tab="tags" data-i18n="tabs.tags"></button>
|
||
|
|
<button class="tab" data-tab="recommend" data-i18n="tabs.recommend"></button>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
${kb()}
|
||
|
|
|
||
|
|
${Ub()}
|
||
|
|
|
||
|
|
<section id="ontology" class="tab-panel">
|
||
|
|
<div class="toolbar">
|
||
|
|
<button id="loadOntologyRegistryBtn" data-i18n="ontology.refresh_registry"></button>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.configured_entity_types"></h2>
|
||
|
|
<div id="ontologyEntities" class="chips"></div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.configured_predicates"></h2>
|
||
|
|
<div id="ontologyPredicates" class="chips"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.registry_entity_types"></h2>
|
||
|
|
<div id="ontologyRegistryEntityTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.registry_relation_types"></h2>
|
||
|
|
<div id="ontologyRegistryRelationTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.triples"></h2>
|
||
|
|
<div id="ontologyTripleTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.proposals"></h2>
|
||
|
|
<div id="ontologyProposalTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="ontology.knowledge_gaps"></h2>
|
||
|
|
<div id="knowledgeGapTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section id="entities" class="tab-panel">
|
||
|
|
<div class="toolbar wrap">
|
||
|
|
<select id="entityTypeFilter"></select>
|
||
|
|
<button id="loadEntitiesBtn" data-i18n="entities.load"></button>
|
||
|
|
</div>
|
||
|
|
<div class="merge-bar">
|
||
|
|
<input id="mergeSourceId" data-i18n-placeholder="entities.merge_source_placeholder" data-i18n-aria-label="entities.merge_source_placeholder" />
|
||
|
|
<input id="mergeTargetId" data-i18n-placeholder="entities.merge_target_placeholder" data-i18n-aria-label="entities.merge_target_placeholder" />
|
||
|
|
<button id="mergeEntitiesBtn" data-i18n="entities.merge"></button>
|
||
|
|
</div>
|
||
|
|
<div id="entityTable" class="table"></div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
${_b()}
|
||
|
|
|
||
|
|
<section id="debug" class="tab-panel">
|
||
|
|
<div class="toolbar">
|
||
|
|
<button id="loadExtractionLogsBtn" data-i18n="debug.refresh_logs"></button>
|
||
|
|
</div>
|
||
|
|
<div id="extractionLogTable" class="claim-list"></div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section id="research" class="tab-panel">
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="research.heading"></h2>
|
||
|
|
<div class="recommend-grid">
|
||
|
|
<label>
|
||
|
|
<span data-i18n="research.goal"></span>
|
||
|
|
<input id="researchGoal" value="Forment perfume ecosystem exploration" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="research.seed_entity"></span>
|
||
|
|
<input id="researchSeedEntityId" placeholder="optional entity id" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="research.max_steps"></span>
|
||
|
|
<input id="researchMaxSteps" type="number" min="1" max="50" value="8" />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span data-i18n="research.min_relevance"></span>
|
||
|
|
<input id="researchMinRelevance" type="number" min="0" max="1" step="0.05" value="0.35" />
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<div class="button-grid">
|
||
|
|
<button id="runResearchBtn" class="primary" data-i18n="research.run"></button>
|
||
|
|
<button id="loadResearchBtn" data-i18n="research.load_sessions"></button>
|
||
|
|
</div>
|
||
|
|
<div id="researchResult" class="mini-log"></div>
|
||
|
|
<div id="researchSessions" class="claim-list"></div>
|
||
|
|
</div>
|
||
|
|
<div class="wide-panel">
|
||
|
|
<h2 data-i18n="research.graph_query"></h2>
|
||
|
|
<div class="toolbar wrap">
|
||
|
|
<select id="graphQueryKind">
|
||
|
|
<option value="trend_summary" data-i18n="research.query.trend_summary"></option>
|
||
|
|
<option value="brand_products" data-i18n="research.query.brand_products"></option>
|
||
|
|
<option value="products_by_tag" data-i18n="research.query.products_by_tag"></option>
|
||
|
|
<option value="relation_summary" data-i18n="research.query.relation_summary"></option>
|
||
|
|
<option value="entity_type_summary" data-i18n="research.query.entity_type_summary"></option>
|
||
|
|
</select>
|
||
|
|
<input id="graphQueryText" data-i18n-placeholder="research.filter_placeholder" />
|
||
|
|
<button id="loadGraphQueryBtn" data-i18n="research.run_query"></button>
|
||
|
|
</div>
|
||
|
|
<div id="graphQueryTable" class="table"></div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section id="tags" class="tab-panel">
|
||
|
|
<div class="toolbar">
|
||
|
|
<button id="loadTagsBtn" data-i18n="tags.load"></button>
|
||
|
|
</div>
|
||
|
|
<div id="tagTable" class="table"></div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section id="recommend" class="tab-panel">
|
||
|
|
<div class="recommend-grid">
|
||
|
|
<label><span data-i18n="recommend.preferred_notes"></span><input id="preferredNotes" value="Bergamot, Musk" /></label>
|
||
|
|
<label><span data-i18n="recommend.avoided_notes"></span><input id="avoidedNotes" value="" /></label>
|
||
|
|
<label><span data-i18n="recommend.preferred_moods"></span><input id="preferredMoods" value="Fresh" /></label>
|
||
|
|
<label><span data-i18n="recommend.season"></span><input id="seasonContext" value="Summer" /></label>
|
||
|
|
<label><span data-i18n="recommend.occasion"></span><input id="occasionContext" value="Daily" /></label>
|
||
|
|
</div>
|
||
|
|
<button id="recommendBtn" class="primary" data-i18n="recommend.test"></button>
|
||
|
|
<div id="recommendTable" class="table"></div>
|
||
|
|
</section>
|
||
|
|
`}function vv(){ew(ue.projectDetail)}function hv(){const t=ue.ontology?.entity_types??[],e=ue.ontology?.predicates??[];q("ontologyEntities").innerHTML=t.map(Pl).join(""),q("ontologyPredicates").innerHTML=e.map(Pl).join(""),q("entityTypeFilter").innerHTML=`<option value="">${z(V("entities.all_types"))}</option>${t.map(r=>`<option value="${z(r)}">${z(r)}</option>`).join("")}`}async function Oo(){if(!ue.selectedProject)return;const t=encodeURIComponent(ue.selectedProject),[e,r,a,n]=await Promise.all([Fe(`/projects/${t}/ontology/registry`),Fe(`/projects/${t}/ontology/proposals?limit=50`),Fe(`/projects/${t}/knowledge-gaps?limit=50`),Fe(`/projects/${t}/ontology/triples?limit=50`)]);q("ontologyRegistryEntityTable").innerHTML=Mt([V("table.type"),V("table.domain"),V("table.status"),V("table.confidence")],(e.entity_types??[]).map(i=>[i.name,i.domain,i.status,Math.round((i.confidence??0)*100)+"%"])),q("ontologyRegistryRelationTable").innerHTML=Mt([V("table.relation"),V("table.domain"),V("table.subject"),V("table.object"),V("table.status")],(e.relation_types??[]).map(i=>[i.name,i.domain,(i.allowed_subject_types??[]).join(", "),(i.allowed_object_types??[]).join(", ")||(i.semantic_constraints?.literal_value?"literal":""),i.status])),q("ontologyProposalTable").innerHTML=Mt([V("table.type"),V("table.name"),V("table.status"),V("table.reason")],(r??[]).map(i=>[i.proposal_type,i.name,i.status,z(i.reason??"")])),q("knowledgeGapTable").innerHTML=Mt([V("table.gap"),V("table.target"),V("table.priority"),V("table.description")],(a??[]).map(i=>[i.gap_type,`${i.target_type??""}: ${i.target_name??""}`,Math.round((i.priority??0)*100)+"%",z(i.description??"")])),q("ontologyTripleTable").innerHTML=Mt([V("table.subject"),V("table.relation"),V("table.object"),V("table.status"),V("table.support")],(n??[]).map(i=>[i.subject??"",i.predicate,i.object??JSON.stringify(i.object_value??""),i.status,i.support_count]))}let sd=[];async function No(){if(!ue.selectedProject)return;const t=q("entityTypeFilter").value,e=t?`?entity_type=${encodeURIComponent(t)}&limit=100`:"?limit=100",r=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/entities${e}`);sd=r,q("metricEntities").textContent=r.length,q("entityTable").innerHTML=Mt([V("table.id"),V("table.type"),V("table.name"),V("table.metadata")],r.map(a=>[`<button class="row-link" data-entity-id="${a.id}">${z(a.id)}</button>`,a.type,z(a.name),`<code>${z(JSON.stringify(a.metadata??{}))}</code>`])),q("entityTable").querySelectorAll("[data-entity-id]").forEach(a=>{a.addEventListener("click",()=>{const n=Number(a.dataset.entityId),i=sd.find(s=>s.id===n);i&&(ue.selection={kind:"entity",data:i},ur())})})}async function aw(){if(!ue.selectedProject)return;const t=Number(q("mergeSourceId").value),e=Number(q("mergeTargetId").value);if(!t||!e||t===e){Ae(V("toast.merge_check"));return}try{const r=await Fe("/entities/merge",{method:"POST",body:JSON.stringify({project_name:ue.selectedProject,source_entity_id:t,target_entity_id:e})});if(!r.ok){Ae(r.error??V("toast.merge_check"));return}Ae(V("toast.entity_merged")),q("mergeSourceId").value="",q("mergeTargetId").value="";const a=new CustomEvent("workspace:refresh");window.dispatchEvent(a)}catch(r){Ae(r.message)}}async function zo(){if(!ue.selectedProject)return;const t=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/extraction-logs?limit=50`);q("extractionLogTable").innerHTML=t.map(nw).join("")}function nw(t){const e=t.validation??{},r=t.page_context??{},n=(e.rejected_claims??[]).slice(0,3).map(i=>`${i.predicate??""}: ${i.reason??""}`).join(" | ");return`
|
||
|
|
<article class="claim-card">
|
||
|
|
<div class="claim-main">
|
||
|
|
<strong>${z(t.extractor_name)}</strong>
|
||
|
|
<span>${z(t.provider)}</span>
|
||
|
|
<span>${z(e.claim_status??"")}</span>
|
||
|
|
<span>accepted ${e.accepted_claim_count??0}</span>
|
||
|
|
<span>rejected ${e.rejected_claim_count??0}</span>
|
||
|
|
</div>
|
||
|
|
<div class="evidence">${z(r.page_type??"")} / ${z(r.crawl_status??"")} / ${z(r.extraction_status??"")}</div>
|
||
|
|
<div class="evidence">${z(t.page_url??"")}</div>
|
||
|
|
<div class="evidence">${z(n)}</div>
|
||
|
|
</article>
|
||
|
|
`}async function iw(){if(!ue.selectedProject)return;q("researchResult").textContent=V("status.research_running");const t=q("researchSeedEntityId").value.trim();try{const e=await Fe("/research/run",{method:"POST",body:JSON.stringify({...Ho(),project_name:ue.selectedProject,seed_entity_id:t?Number(t):null,goal:q("researchGoal").value.trim()||"Semantic ontology exploration",max_depth:Number(q("siteMaxDepth").value||2),max_steps:Number(q("researchMaxSteps").value||8),max_branch:8,min_relevance:Number(q("researchMinRelevance").value||.35),same_domain_only:q("sameDomainOnly").checked,analyze_page_types:["ProductPage","BrandStoryPage","ReviewPage"]})});q("researchResult").textContent=`Research ${e.status}: explored ${e.explored_count}, analyzed ${e.analyzed_count}, queued ${e.queued_count}, skipped ${e.skipped_count}`,Ae(V("toast.research_completed"));const r=new CustomEvent("workspace:refresh");window.dispatchEvent(r)}catch(e){q("researchResult").textContent=`${V("toast.research_failed")}: ${e.message}`,Ae(V("toast.research_failed"))}}async function $o(){if(!ue.selectedProject)return;const t=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/research/sessions?limit=20`);q("researchSessions").innerHTML=t.map(sw).join("")}function sw(t){const e=t.history??[],r=e.length?e[e.length-1]:null,n=(t.memory??{}).conflicts??[];return`
|
||
|
|
<article class="claim-card">
|
||
|
|
<div class="claim-main">
|
||
|
|
<strong>${z(t.name??"Research session")}</strong>
|
||
|
|
<span>${z(t.status??"")}</span>
|
||
|
|
<span>history ${e.length}</span>
|
||
|
|
<span>queue ${(t.queue??[]).length}</span>
|
||
|
|
<span>conflicts ${n.length}</span>
|
||
|
|
</div>
|
||
|
|
<div class="evidence">${z(t.goal??"")}</div>
|
||
|
|
<div class="evidence">seed ${z(t.seed??"")}</div>
|
||
|
|
<div class="evidence">${z(r?.outcome?.reason??r?.outcome?.status??"")}</div>
|
||
|
|
</article>
|
||
|
|
`}async function Fo(){if(!ue.selectedProject)return;const t=q("graphQueryKind")?.value??"trend_summary",e=q("graphQueryText")?.value.trim()??"";let r=`kind=${encodeURIComponent(t)}`;t==="brand_products"&&e&&(r+=`&brand=${encodeURIComponent(e)}`),t==="products_by_tag"&&e&&(r+=`&tag=${encodeURIComponent(e)}`);const a=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/graph/query?${r}`);q("graphQueryTable").innerHTML=ow(t,a)}function ow(t,e){return t==="brand_products"?Mt([V("table.product"),V("table.type"),V("table.brand"),V("table.confidence")],e.map(r=>[r.product,r.product_type,r.brand??"",Math.round(r.confidence*100)+"%"])):t==="products_by_tag"?Mt([V("table.product"),V("table.predicate"),V("table.tag"),V("table.confidence")],e.map(r=>[r.product,r.predicate,r.tag,Math.round(r.confidence*100)+"%"])):t==="relation_summary"?Mt([V("table.predicate"),V("table.status"),V("table.support"),V("table.confidence")],e.map(r=>[r.predicate,r.status,r.support_count,Math.round((r.max_confidence??0)*100)+"%"])):t==="entity_type_summary"?Mt([V("table.type"),V("table.count")],e.map(r=>[r.entity_type,r.count])):Mt([V("table.predicate"),V("table.name"),V("table.type"),V("table.support"),V("table.confidence")],e.map(r=>[r.predicate,r.name,r.entity_type,r.support_count,Math.round((r.max_confidence??0)*100)+"%"]))}async function qo(){if(!ue.selectedProject)return;const t=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/recommendation-tags`);q("tagTable").innerHTML=Mt([V("table.predicate"),V("table.type"),V("table.name"),V("table.support"),V("table.confidence")],t.map(e=>[e.predicate,e.type,e.name,e.support_count,Math.round((e.max_confidence??0)*100)+"%"]))}async function lw(){if(ue.selectedProject)try{const t=await Fe("/recommend",{method:"POST",body:JSON.stringify({project_name:ue.selectedProject,target_entity_type:ue.projectDetail?.config?.recommendation?.target_entity_type??"Perfume",preferences:{preferred_notes:Di(q("preferredNotes").value),avoided_notes:Di(q("avoidedNotes").value),preferred_moods:Di(q("preferredMoods").value),season_context:q("seasonContext").value.trim()||null,occasion_context:q("occasionContext").value.trim()||null},limit:10})});q("recommendTable").innerHTML=Mt([V("table.name"),V("table.type"),V("table.score"),V("table.reasons")],t.map(e=>[e.name,e.entity_type,e.score,(e.reasons??[]).join(", ")]))}catch(t){Ae(t.message)}}let lo=null,pv="",qa=null;function uw(){const t=q("globalSearchInput"),e=q("globalSearchPanel");xt(t.parentElement),t.addEventListener("input",()=>{const r=t.value.trim();lo&&window.clearTimeout(lo),lo=window.setTimeout(()=>cw(r),200)}),t.addEventListener("focus",()=>{pv?Vo(qa):gv(),e.hidden=!1}),t.addEventListener("keydown",r=>{r.key==="Escape"&&(t.value="",e.hidden=!0,t.blur()),r.key==="ArrowDown"&&(r.preventDefault(),e.querySelector("[data-search-item]")?.focus())}),document.addEventListener("click",r=>{!e.contains(r.target)&&r.target!==t&&(e.hidden=!0)}),document.addEventListener("keydown",r=>{r.key==="/"&&!r.target.matches("input, textarea, select")&&(r.preventDefault(),t.focus(),t.select())}),fr(()=>{xt(t.parentElement),qa&&Vo(qa)})}async function cw(t){if(!ue.selectedProject)return;pv=t;const e=q("globalSearchPanel");if(!t){gv();return}try{qa=await Fe(`/projects/${encodeURIComponent(ue.selectedProject)}/search?q=${encodeURIComponent(t)}&limit=8`),Vo(qa)}catch(r){e.innerHTML=`<div class="search-empty muted">${z(r.message)}</div>`,e.hidden=!1}}function gv(){const t=q("globalSearchPanel");t.innerHTML=`<div class="search-empty muted">${z(V("search.hint"))}</div>`,t.hidden=!1}function Vo(t){const e=q("globalSearchPanel");if(!t)return;const r=[{key:"entities",items:t.entities,render:dw},{key:"claims",items:t.claims,render:fw},{key:"pages",items:t.pages,render:vw},{key:"predicates",items:t.predicates,render:hw}].filter(a=>(a.items??[]).length>0);r.length?(e.innerHTML=r.map(a=>`
|
||
|
|
<div class="search-group">
|
||
|
|
<div class="search-group-head">${z(V(`search.group.${a.key}`))}</div>
|
||
|
|
${a.items.map((n,i)=>a.render(n,i)).join("")}
|
||
|
|
</div>
|
||
|
|
`).join(""),e.querySelectorAll("[data-search-item]").forEach(a=>{a.addEventListener("click",()=>pw(a.dataset.searchItem,JSON.parse(a.dataset.payload))),a.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),a.click()),n.key==="ArrowDown"&&(n.preventDefault(),(a.nextElementSibling||e.querySelector("[data-search-item]"))?.focus?.()),n.key==="ArrowUp"&&(n.preventDefault(),(a.previousElementSibling||e.querySelector("[data-search-item]:last-of-type"))?.focus?.()),n.key==="Escape"&&q("globalSearchInput").focus()})})):e.innerHTML=`<div class="search-empty muted">${z(V("search.empty"))}</div>`,e.hidden=!1}function dw(t){return`
|
||
|
|
<button class="search-item" data-search-item="entity" data-payload='${ki(JSON.stringify(t))}'>
|
||
|
|
<span class="search-item-main">${z(t.name)}</span>
|
||
|
|
<span class="search-item-meta">${z(t.type??"")}</span>
|
||
|
|
</button>
|
||
|
|
`}function fw(t){const e=t.object_value?JSON.stringify(t.object_value):"";return`
|
||
|
|
<button class="search-item" data-search-item="claim" data-payload='${ki(JSON.stringify(t))}'>
|
||
|
|
<span class="search-item-main">${z(t.subject??"")} · ${z(t.predicate??"")} ${z(e)}</span>
|
||
|
|
<span class="search-item-meta">${Math.round((t.confidence??0)*100)}%</span>
|
||
|
|
</button>
|
||
|
|
`}function vw(t){return`
|
||
|
|
<button class="search-item" data-search-item="page" data-payload='${ki(JSON.stringify(t))}'>
|
||
|
|
<span class="search-item-main">${z(t.title??t.url??"")}</span>
|
||
|
|
<span class="search-item-meta">${z(t.url??"")}</span>
|
||
|
|
</button>
|
||
|
|
`}function hw(t){return`
|
||
|
|
<button class="search-item" data-search-item="predicate" data-payload='${ki(JSON.stringify(t))}'>
|
||
|
|
<span class="search-item-main">${z(t)}</span>
|
||
|
|
</button>
|
||
|
|
`}function ki(t){return String(t).replaceAll("&","&").replaceAll("'","'").replaceAll('"',""")}function pw(t,e){const r=q("globalSearchPanel");if(r.hidden=!0,t==="entity")uo("entities"),ue.selection={kind:"entity",data:{id:e.id,name:e.name,type:e.type,metadata:{}}},ur();else if(t==="claim")uo("claims"),ue.selection={kind:"claim",data:{...e,object:void 0}},ur();else if(t==="page")e.url&&window.open(e.url,"_blank","noreferrer");else if(t==="predicate"){uo("graph");const a=q("graphPredicate");a&&(a.value=e,a.dispatchEvent(new Event("change")))}}function uo(t){document.querySelectorAll(".tab").forEach(e=>e.classList.toggle("active",e.dataset.tab===t)),document.querySelectorAll(".tab-panel").forEach(e=>e.classList.toggle("active",e.id===t))}const gw=document.getElementById("app"),ml=Rv(gw);Av(ml.sidebarHost,{onProjectChanged:bl,onCrawlUpdate:()=>ur(),refreshAll:wl});tw(ml.workspaceHost);Hv(ml.inspectorHost);uw();document.getElementById("refreshBtn").addEventListener("click",bl);window.addEventListener("project:select",t=>yv(t.detail));window.addEventListener("workspace:refresh",wl);fr(()=>ur());bl().catch(t=>Ae(t.message));async function bl(){ue.projects=await Fe("/projects"),!ue.selectedProject&&ue.projects.length&&(ue.selectedProject=ue.projects[0].name),Go(),ue.selectedProject&&await yv(ue.selectedProject)}async function yv(t){ue.selectedProject=t,ue.projectDetail=await Fe(`/projects/${encodeURIComponent(t)}`),ue.ontology=await Fe(`/ontology/${encodeURIComponent(ue.projectDetail.domain)}`),Go(),Mv(ue.projectDetail),vv(),hv(),ur(),await wl()}async function wl(){if(!ue.selectedProject)return;const t=[["pipeline",yl],["ontology registry",Oo],["entities",No],["claims workbench",Ta],["extraction logs",zo],["research sessions",$o],["graph query",Fo],["tags",qo],["graph",gl]];await Promise.all(t.map(async([e,r])=>{try{await r()}catch(a){console.warn(`${e} load failed`,a)}})),ur()}
|
||
|
|
//# sourceMappingURL=index-fQcYiMpd.js.map
|