wiki-plugin-similarity 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/similarity.js +35 -15
- package/client/similarity.js.map +3 -3
- package/package.json +20 -6
package/client/similarity.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/* wiki-plugin-similarity - 0.1.
|
|
2
|
-
(()=>{var
|
|
3
|
-
`)){let
|
|
1
|
+
/* wiki-plugin-similarity - 0.1.2 - Tue, 09 Jun 2026 19:41:11 GMT */
|
|
2
|
+
(()=>{var I={high:.78,medium:.68,low:.58},L=I.medium,E=10,z=t=>{let s=[],e=null,n=null,i="search";for(let g of t.split(`
|
|
3
|
+
`)){let a=g.trim();if(!a||a.startsWith("#"))continue;let o=a.toUpperCase();if(o==="LIST"||o.startsWith("LIST ")){!s.length&&i==="search"&&(i="list");continue}if(o.startsWith("SIMILAR:")){let m=a.split(":")[1].trim().toLowerCase();e=I[m]??L,!s.length&&i==="search"&&(i="similar");continue}if(o.startsWith("THRESHOLD:")){e=parseFloat(a.split(":")[1])||L;continue}if(o.startsWith("LIMIT:")){n=parseInt(a.split(":")[1])||E;continue}s.push(a)}return{mode:i,specs:s,threshold:e??L,limit:n??E}},M=t=>t.includes("*")||t.includes("?"),U=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),v=new Map,F=async(t,s)=>{if(v.has(t))return v.get(t);let e=`${s}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,n=await fetch(e);if(!n.ok)throw new Error(`indexed-domains failed: ${n.status}`);let i=await n.json();return v.set(t,i),i},W=async(t,s)=>{t.length||(t=[window.location.hostname]);let e=new Set,n=[];for(let i of t)if(i==="*"||M(i))for(let g of await F(i,s))e.has(g.domain)||(e.add(g.domain),n.push(g));else e.has(i)||(e.add(i),n.push({domain:i,page_count:null}));return n},S=new Map,P=()=>{let t=window.location.hostname;return t==="localhost"||t.endsWith(".localhost")||t==="127.0.0.1"},_=t=>P()?`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`:`http://${t}/system/semantic-vectors.json`,R=async t=>{if(S.has(t))return S.get(t);let s=await fetch(_(t));if(!s.ok)return[];let e=await s.json();return S.set(t,e),e},k=async(t,s)=>{let e=await fetch(`${s}/system/embed.json?text=${encodeURIComponent(t)}`);if(!e.ok)throw new Error(`embed failed: ${e.status}`);return(await e.json()).vector},A=async(t,s)=>{let n=(await R(s)).find(i=>i.slug===t);return n?n.vector:null},j=(t,s,{threshold:e,limit:n,excludeSlug:i,excludeDomain:g})=>{let a=[];for(let{domain:o,pages:m}of s)for(let{slug:l,title:h,vector:d}of m){if(l===i&&o===g)continue;let f=0;for(let c=0;c<t.length;c++)f+=t[c]*d[c];f>=e&&a.push({domain:o,slug:l,title:h,score:f})}return a.sort((o,m)=>m.score-o.score),a.slice(0,n)},T=async(t,s)=>{let e=await W(t,s);return(await Promise.all(e.map(async({domain:i})=>({domain:i,pages:await R(i)})))).filter(i=>i.pages.length>0)},C=`
|
|
4
4
|
.sim-form { display:flex; gap:6px; margin-bottom:8px; }
|
|
5
5
|
.sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }
|
|
6
6
|
.sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;
|
|
@@ -14,25 +14,45 @@
|
|
|
14
14
|
.sim-link { font-size:14px; color:#406; flex:1; }
|
|
15
15
|
.sim-domain { font-size:11px; color:#999; }
|
|
16
16
|
.similar-results h3 { margin:4px 0 6px; font-size:14px; color:#555; }
|
|
17
|
+
.similar-results h3 small { font-size:12px; color:#888; font-weight:normal; margin-left:6px; }
|
|
17
18
|
.similar-results ul { margin:0; padding-left:18px; }
|
|
18
19
|
.similar-results li { font-size:14px; padding:2px 0; }
|
|
19
20
|
.similar-results .sim-domain { margin-left:6px; }
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
.sim-list table { border-collapse:collapse; width:100%; font-size:13px; }
|
|
22
|
+
.sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;
|
|
23
|
+
border-bottom:1px solid #ddd; }
|
|
24
|
+
.sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }
|
|
25
|
+
.sim-list td:last-child { text-align:right; color:#999; font-size:11px; }
|
|
26
|
+
.sim-list .sim-flag { margin-right:6px; }
|
|
27
|
+
`,q=(t,s)=>`<img class="sim-flag remote" src="${window.wiki.site(t).flag()}"
|
|
28
|
+
title="${t} \u2014 score ${s.toFixed(3)}"
|
|
29
|
+
data-site="${t}">`,D=(t,s,e,n)=>`<a class="sim-link" data-title="${e}" data-slug="${s}" data-site="${t}" href="#">${q(t,n)} ${e}</a>`,O=(t,s)=>{let{mode:e,specs:n,threshold:i,limit:g}=z(s?.text||"");if(e==="list"){let a=n.length?n.join(", "):"*";t.html(`
|
|
30
|
+
<style>${C}</style>
|
|
31
|
+
<div class="similarity" data-id="${s.id}">
|
|
32
|
+
<div class="sim-status">Loading indexed domains (${a})\u2026</div>
|
|
33
|
+
<div class="sim-list"></div>
|
|
34
|
+
</div>`)}else if(e==="similar"){let a=n.length?n.join(", "):"current domain";t.html(`
|
|
35
|
+
<style>${C}</style>
|
|
36
|
+
<div class="similarity" data-id="${s.id}">
|
|
37
|
+
<div class="sim-status">Finding similar pages across ${a}\u2026</div>
|
|
26
38
|
<div class="sim-results"></div>
|
|
27
|
-
</div>`)}else{let
|
|
28
|
-
<style>${
|
|
29
|
-
<div class="similarity" data-id="${
|
|
39
|
+
</div>`)}else{let a=n.length?n.join(", "):"(current domain)";t.html(`
|
|
40
|
+
<style>${C}</style>
|
|
41
|
+
<div class="similarity" data-id="${s.id}">
|
|
30
42
|
<div class="sim-form">
|
|
31
43
|
<input class="sim-input" type="text" placeholder="Search wiki pages\u2026" />
|
|
32
44
|
<button class="sim-btn">Search</button>
|
|
33
45
|
</div>
|
|
34
|
-
<div class="sim-status">Domains: ${
|
|
46
|
+
<div class="sim-status">Domains: ${a}</div>
|
|
35
47
|
<div class="sim-results"></div>
|
|
36
|
-
</div>`)}},V=(t,
|
|
37
|
-
|
|
48
|
+
</div>`)}},V=(t,s)=>{let{mode:e,specs:n,threshold:i,limit:g}=z(s?.text||""),a=window.location.origin,o=t.find(".sim-status")[0];if(t.on("dblclick",()=>window.wiki.textEditor(t,s)),t.on("click",".sim-link",function(m){m.preventDefault();let l=$(this);window.wiki.doInternalLink(l.data("title"),t.parents(".page"),l.data("site"))}),e==="list"){let m=t.find(".sim-list")[0],l=n.length?n.join(","):"*";(async()=>{try{let d=`${a}/system/indexed-domains.json?pattern=${encodeURIComponent(l)}`,f=await fetch(d);if(!f.ok)throw new Error(`indexed-domains failed: ${f.status}`);let c=await f.json();if(!c.length){o.textContent="No indexed domains found";return}let p=c.reduce((r,u)=>r+(u.page_count||0),0);o.textContent=`${c.length} domains \u2014 ${p.toLocaleString()} pages`,m.innerHTML=`<table>
|
|
49
|
+
<tr><th>Domain</th><th>Pages</th></tr>
|
|
50
|
+
${c.map(({domain:r,page_count:u})=>`
|
|
51
|
+
<tr>
|
|
52
|
+
<td><img class="sim-flag remote" src="${window.wiki.site(r).flag()}"
|
|
53
|
+
title="${r}" data-site="${r}"> ${r}</td>
|
|
54
|
+
<td>${u!=null?u.toLocaleString():"\u2014"}</td>
|
|
55
|
+
</tr>`).join("")}
|
|
56
|
+
</table>`}catch(d){o.textContent=`Error: ${d.message}`}})()}else if(e==="similar")(async()=>{try{let l=t.parents(".page"),h=l.find(".title").text().trim()||document.title,d=U(h),f=window.location.hostname,c=await T(n,a),p=c.reduce((x,w)=>x+w.pages.length,0);o.textContent=`Searching ${p.toLocaleString()} pages\u2026`;let r=await A(d,f);if(!r){o.textContent="Embedding page (not yet indexed)\u2026";let x=l.find(".item").map((w,y)=>$(y).text().trim()).get().filter(Boolean).join(`
|
|
57
|
+
`);r=await k(x||h,a)}let u=j(r,c,{threshold:i,limit:g,excludeSlug:d,excludeDomain:f});if(!u.length){o.textContent=`No similar pages found above threshold ${i}`;return}let b=t.find(".sim-results")[0];b.innerHTML=`<h3>Similar Pages <small>${u.length} found</small></h3><ul>${u.map(({domain:x,slug:w,title:y,score:H})=>`<li>${D(x,w,y,H)}</li>`).join("")}</ul>`,o.textContent=""}catch(l){o.textContent=`Error: ${l.message}`}})();else{let m=t.find(".sim-input")[0],l=t.find(".sim-btn")[0],h=t.find(".sim-results")[0],d=null;(async()=>{try{o.textContent="Resolving domains\u2026",d=await T(n,a);let p=d.reduce((r,u)=>r+u.pages.length,0);o.textContent=`Ready \u2014 ${p.toLocaleString()} pages across ${d.length} domains`}catch(p){o.textContent=`Load error: ${p.message}`}})();let c=async()=>{let p=m.value.trim();if(!(!p||!d)){l.disabled=!0,o.textContent="Embedding query\u2026",h.innerHTML="";try{let r=await k(p,a),u=j(r,d,{threshold:0,limit:g,excludeSlug:null,excludeDomain:null});h.innerHTML=u.map(({domain:b,slug:x,title:w,score:y})=>`<div class="sim-result">${D(b,x,w,y)}</div>`).join(""),o.textContent=`Top ${u.length} results for "${p}"`}catch(r){o.textContent=`Error: ${r.message}`}finally{l.disabled=!1}}};l.addEventListener("click",c),m.addEventListener("keydown",p=>{p.key==="Enter"&&c()})}};typeof window<"u"&&(window.plugins=window.plugins||{},window.plugins.similarity={emit:O,bind:V});})();
|
|
38
58
|
//# sourceMappingURL=similarity.js.map
|
package/client/similarity.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/similarity.js"],
|
|
4
|
-
"sourcesContent": ["// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// SIMILAR: high threshold preset AND ambient mode trigger\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n//\n// Mode is determined by DSL (Ward's ALL-CAPS convention):\n// SIMILAR: as the FIRST meaningful line \u2192 ambient mode (no search form;\n// the item automatically finds pages similar to the current page)\n// Domain specs first, or empty text \u2192 search form mode (user types query)\n//\n// Server endpoints required:\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// GET /system/embed.json?text=query\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit }\n//\n// mode: 'similar' if SIMILAR: is the first meaningful line (ambient auto-run),\n// 'search' otherwise (interactive search form).\n//\n// Ward's convention: ALL-CAPS keyword as first word signals a mode switch.\n// Placing SIMILAR: first makes the item ambient; placing domain specs first\n// (or leaving text empty) keeps it as an interactive search form.\n\nconst parseDSL = text => {\n const specs = []\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (upper.startsWith('SIMILAR:')) {\n const level = line.split(':')[1].trim().toLowerCase()\n threshold = SIMILAR_THRESHOLDS[level] ?? DEFAULT_THRESHOLD\n // SIMILAR: as the first meaningful line \u2192 ambient mode\n if (!specs.length && mode === 'search') mode = 'similar'\n continue\n }\n if (upper.startsWith('THRESHOLD:')) {\n threshold = parseFloat(line.split(':')[1]) || DEFAULT_THRESHOLD\n continue\n }\n if (upper.startsWith('LIMIT:')) {\n limit = parseInt(line.split(':')[1]) || DEFAULT_LIMIT\n continue\n }\n // Anything else is a domain spec\n specs.push(line)\n }\n\n return {\n mode,\n specs,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Detect whether the wiki is running on localhost (or a .localhost subdomain).\n// On localhost we proxy all domain vector fetches through the local server's\n// ?domain= parameter \u2014 this reads vectors directly from the farm folder on disk,\n// avoiding any need for remote servers to have this plugin installed, and\n// sidestepping CORS entirely.\nconst isLocalhost = () => {\n const h = window.location.hostname\n return h === 'localhost' || h.endsWith('.localhost') || h === '127.0.0.1'\n}\n\nconst vectorUrl = domain => {\n if (isLocalhost()) {\n // Route through our local server \u2014 serves any farm domain from disk\n return `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n }\n // On a real server, fetch directly from the remote wiki\n return `http://${domain}/system/semantic-vectors.json`\n}\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json?text=${encodeURIComponent(text)}`)\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n if (mode === 'similar') {\n const label = specs.length ? specs.join(', ') : 'current domain'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n const results = div.find('.sim-results')[0]\n\n div.on('dblclick', () => window.wiki.textEditor(div, item))\n\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n window.wiki.doInternalLink($a.data('title'), div.parents('.page'), $a.data('site'))\n })\n\n if (mode === 'similar') {\n // Ambient mode \u2014 run automatically, no search form\n const run = async () => {\n try {\n const $page = div.parents('.page')\n const pageTitle = $page.find('.title').text().trim() || document.title\n const currentSlug = slugify(pageTitle)\n const currentDomain = window.location.hostname\n\n const domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(currentSlug, currentDomain)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = $page.find('.item').map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || pageTitle, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: currentSlug, excludeDomain: currentDomain,\n })\n\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n results.innerHTML = `<h3>Similar Pages</h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul>`\n status.textContent = `${scored.length} similar pages found`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else {\n // Search form mode \u2014 preload domains, then wait for user input\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let domainEntries = null\n\n const preload = async () => {\n try {\n status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Ready \u2014 ${total.toLocaleString()} pages across ${domainEntries.length} domains`\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n }\n preload()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold: 0, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('')\n status.textContent = `Top ${scored.length} results for \"${query}\"`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "threshold", "limit", "mode", "raw", "line", "upper", "level", "isGlob", "spec", "slugify", "title", "domainCache", "resolveDomainsForSpec", "origin", "url", "res", "list", "resolveDomains", "seen", "result", "item", "vectorCache", "isLocalhost", "h", "vectorUrl", "domain", "loadVectors", "data", "getEmbedding", "lookupPageVector", "slug", "entry", "p", "cosineScan", "queryVec", "domainEntries", "excludeSlug", "excludeDomain", "results", "pages", "vector", "dot", "i", "b", "loadDomainEntries", "domains", "e", "STYLES", "siteFlag", "score", "simLink", "emit", "div", "label", "bind", "status", "$a", "$page", "pageTitle", "currentSlug", "currentDomain", "total", "
|
|
4
|
+
"sourcesContent": ["// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required:\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// GET /system/embed.json?text=query\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit }\n//\n// mode: 'similar' if SIMILAR: is the first meaningful line (ambient auto-run),\n// 'search' otherwise (interactive search form).\n//\n// Ward's convention: ALL-CAPS keyword as first word signals a mode switch.\n// Placing SIMILAR: first makes the item ambient; placing domain specs first\n// (or leaving text empty) keeps it as an interactive search form.\n\nconst parseDSL = text => {\n const specs = []\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (upper === 'LIST' || upper.startsWith('LIST ')) {\n // LIST as first meaningful line \u2192 domain listing mode\n if (!specs.length && mode === 'search') mode = 'list'\n continue\n }\n if (upper.startsWith('SIMILAR:')) {\n const level = line.split(':')[1].trim().toLowerCase()\n threshold = SIMILAR_THRESHOLDS[level] ?? DEFAULT_THRESHOLD\n // SIMILAR: as the first meaningful line \u2192 ambient mode\n if (!specs.length && mode === 'search') mode = 'similar'\n continue\n }\n if (upper.startsWith('THRESHOLD:')) {\n threshold = parseFloat(line.split(':')[1]) || DEFAULT_THRESHOLD\n continue\n }\n if (upper.startsWith('LIMIT:')) {\n limit = parseInt(line.split(':')[1]) || DEFAULT_LIMIT\n continue\n }\n // Anything else is a domain spec (glob or explicit domain)\n specs.push(line)\n }\n\n return {\n mode,\n specs,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Detect whether the wiki is running on localhost (or a .localhost subdomain).\n// On localhost we proxy all domain vector fetches through the local server's\n// ?domain= parameter \u2014 this reads vectors directly from the farm folder on disk,\n// avoiding any need for remote servers to have this plugin installed, and\n// sidestepping CORS entirely.\nconst isLocalhost = () => {\n const h = window.location.hostname\n return h === 'localhost' || h.endsWith('.localhost') || h === '127.0.0.1'\n}\n\nconst vectorUrl = domain => {\n if (isLocalhost()) {\n // Route through our local server \u2014 serves any farm domain from disk\n return `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n }\n // On a real server, fetch directly from the remote wiki\n return `http://${domain}/system/semantic-vectors.json`\n}\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json?text=${encodeURIComponent(text)}`)\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results h3 small { font-size:12px; color:#888; font-weight:normal; margin-left:6px; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n if (mode === 'list') {\n const label = specs.length ? specs.join(', ') : '*'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'similar') {\n const label = specs.length ? specs.join(', ') : 'current domain'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n\n div.on('dblclick', () => window.wiki.textEditor(div, item))\n\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n window.wiki.doInternalLink($a.data('title'), div.parents('.page'), $a.data('site'))\n })\n\n if (mode === 'list') {\n // List all indexed domains matching the spec patterns (default: all)\n const listDiv = div.find('.sim-list')[0]\n const patterns = specs.length ? specs.join(',') : '*'\n const run = async () => {\n try {\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(patterns)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const domains = await res.json()\n if (!domains.length) {\n status.textContent = 'No indexed domains found'\n return\n }\n const totalPages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n status.textContent = `${domains.length} domains \u2014 ${totalPages.toLocaleString()} pages`\n listDiv.innerHTML = `<table>\n <tr><th>Domain</th><th>Pages</th></tr>\n ${domains.map(({ domain, page_count }) => `\n <tr>\n <td><img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain}\" data-site=\"${domain}\"> ${domain}</td>\n <td>${page_count != null ? page_count.toLocaleString() : '\u2014'}</td>\n </tr>`).join('')}\n </table>`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else if (mode === 'similar') {\n // Ambient mode \u2014 run automatically, no search form\n const run = async () => {\n try {\n const $page = div.parents('.page')\n const pageTitle = $page.find('.title').text().trim() || document.title\n const currentSlug = slugify(pageTitle)\n const currentDomain = window.location.hostname\n\n const domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(currentSlug, currentDomain)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = $page.find('.item').map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || pageTitle, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: currentSlug, excludeDomain: currentDomain,\n })\n\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n const results = div.find('.sim-results')[0]\n results.innerHTML = `<h3>Similar Pages <small>${scored.length} found</small></h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul>`\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else {\n // Search form mode \u2014 preload domains, then wait for user input\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n const preload = async () => {\n try {\n status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Ready \u2014 ${total.toLocaleString()} pages across ${domainEntries.length} domains`\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n }\n preload()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold: 0, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('')\n status.textContent = `Top ${scored.length} results for \"${query}\"`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "threshold", "limit", "mode", "raw", "line", "upper", "level", "isGlob", "spec", "slugify", "title", "domainCache", "resolveDomainsForSpec", "origin", "url", "res", "list", "resolveDomains", "seen", "result", "item", "vectorCache", "isLocalhost", "h", "vectorUrl", "domain", "loadVectors", "data", "getEmbedding", "lookupPageVector", "slug", "entry", "p", "cosineScan", "queryVec", "domainEntries", "excludeSlug", "excludeDomain", "results", "pages", "vector", "dot", "i", "a", "b", "loadDomainEntries", "domains", "e", "STYLES", "siteFlag", "score", "simLink", "emit", "div", "label", "bind", "status", "$a", "listDiv", "patterns", "totalPages", "n", "d", "page_count", "$page", "pageTitle", "currentSlug", "currentDomain", "total", "qVec", "pageText", "_", "el", "scored", "input", "btn", "doSearch", "query"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-plugin-similarity",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Federated Wiki plugin — semantic similarity search across wiki domains using prebuilt vector indexes",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wiki",
|
|
7
|
+
"federated wiki",
|
|
8
|
+
"plugin",
|
|
9
|
+
"similarity",
|
|
10
|
+
"semantic search",
|
|
11
|
+
"embeddings"
|
|
12
|
+
],
|
|
6
13
|
"author": {
|
|
7
14
|
"name": "David Bovill",
|
|
8
15
|
"url": "https://github.com/Hitchhikers-Guide-to-the-Galaxy"
|
|
9
16
|
},
|
|
10
17
|
"license": "MIT",
|
|
11
18
|
"type": "module",
|
|
12
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"client",
|
|
21
|
+
"server",
|
|
22
|
+
"pages",
|
|
23
|
+
"factory.json"
|
|
24
|
+
],
|
|
13
25
|
"repository": {
|
|
14
26
|
"type": "git",
|
|
15
27
|
"url": "git+https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity.git"
|
|
16
28
|
},
|
|
17
|
-
"engines": {
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.x"
|
|
31
|
+
},
|
|
18
32
|
"scripts": {
|
|
19
33
|
"build": "npm run clean && npm run test && node --no-warnings scripts/build-client.js",
|
|
20
|
-
"dev":
|
|
34
|
+
"dev": "node --no-warnings scripts/build-client.js --watch",
|
|
21
35
|
"about": "wiki -p 3010 -d . --security_legacy",
|
|
22
36
|
"clean": "rm -f client/similarity.js client/similarity.js.map",
|
|
23
|
-
"test":
|
|
37
|
+
"test": "node --test",
|
|
24
38
|
"prepublishOnly": "npm run build"
|
|
25
39
|
},
|
|
26
40
|
"devDependencies": {
|