@zsoltcsaszti/lens 0.2.0
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/README.md +180 -0
- package/dist/lens.esm.js +1 -0
- package/dist/lens.umd.js +1 -0
- package/package.json +46 -0
- package/sdk/lens.js +660 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Lens SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zsolt/lens)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Reader intelligence for publishers. One script tag. Zero tracking cookies.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
**Via CDN (recommended for most publishers):**
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<script
|
|
16
|
+
src="https://unpkg.com/@zsolt/lens/dist/lens.umd.js"
|
|
17
|
+
data-site-id="YOUR_SITE_ID"
|
|
18
|
+
data-api="https://your-lens-server.com"
|
|
19
|
+
data-mode="emphasis">
|
|
20
|
+
</script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Paste before `</body>` on every page. That's it — no API keys for readers, no accounts, no configuration files.
|
|
24
|
+
|
|
25
|
+
**Via npm (for bundler-based setups):**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @zsolt/lens
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import '@zsolt/lens' // side-effect import — auto-initialises from data-* attributes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**WordPress:** copy [`wordpress/lens-analytics/`](wordpress/lens-analytics/) into `wp-content/plugins/`, activate, and configure under **Settings → Lens Analytics**.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## What it does
|
|
40
|
+
|
|
41
|
+
Lens silently observes how each visitor reads — which sections they dwell on, re-read, copy — and builds an anonymous interest profile in their browser. After a couple of visits, it subtly adapts section emphasis so each reader sees what matters most to them first.
|
|
42
|
+
|
|
43
|
+
**What Lens never does:**
|
|
44
|
+
- Never rewrites a word of your content
|
|
45
|
+
- Never sends browsing history or personal data anywhere
|
|
46
|
+
- Never requires readers to sign up or install anything
|
|
47
|
+
|
|
48
|
+
The visitor profile lives entirely in `localStorage`. Only anonymised engagement metrics (section dwell times, scroll depth) are sent to the analytics server.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Live demos
|
|
53
|
+
|
|
54
|
+
Run `npm start` then open:
|
|
55
|
+
|
|
56
|
+
| URL | What you see |
|
|
57
|
+
|-----|-------------|
|
|
58
|
+
| `http://localhost:3000/` | Demo landing page |
|
|
59
|
+
| `http://localhost:3000/demo/article-tech.html` | Tech deep-dive article |
|
|
60
|
+
| `http://localhost:3000/demo/article-breaking-news.html` | Breaking news style |
|
|
61
|
+
| `http://localhost:3000/demo/article-magazine.html` | Long-form magazine essay |
|
|
62
|
+
| `http://localhost:3000/demo/article-tutorial.html` | Step-by-step tutorial |
|
|
63
|
+
| `http://localhost:3000/dashboard?site=demo` | Publisher analytics dashboard |
|
|
64
|
+
| `http://localhost:3000/public/index.html` | Privacy positioning page |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Run the analytics server
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd lens-sdk
|
|
72
|
+
npm install
|
|
73
|
+
npm start # production
|
|
74
|
+
npm run dev # auto-restart on file change
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Server starts at `http://localhost:3000`. Set the `PORT` env variable to change it.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Script tag options
|
|
82
|
+
|
|
83
|
+
| Attribute | Values | Default | Description |
|
|
84
|
+
|-----------|--------|---------|-------------|
|
|
85
|
+
| `data-site-id` | any string | `demo` | Your unique publisher identifier |
|
|
86
|
+
| `data-api` | URL | — | Your Lens server endpoint for analytics |
|
|
87
|
+
| `data-mode` | `emphasis` · `collapse` · `both` · `off` | `emphasis` | How to adapt presentation |
|
|
88
|
+
| `data-debug` | `true` · `false` | `false` | Show relevance scores on each section |
|
|
89
|
+
|
|
90
|
+
**Modes:**
|
|
91
|
+
- `emphasis` — subtle left accent on highly relevant sections
|
|
92
|
+
- `collapse` — folds low-relevance sections to 2 lines with "Show more"
|
|
93
|
+
- `both` — emphasis on high, collapse on low
|
|
94
|
+
- `off` — analytics only, no visual changes
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Dashboard
|
|
99
|
+
|
|
100
|
+
The dashboard shows what your readers actually engage with — not just pageviews.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
http://localhost:3000/dashboard?site=YOUR_SITE_ID
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**What's included:**
|
|
107
|
+
- Section engagement ranked by average dwell time
|
|
108
|
+
- **Section interest heatmap** — sections in article order, coloured cold→hot by attention
|
|
109
|
+
- The gap between word count and actual attention (the insight that drives editorial decisions)
|
|
110
|
+
- Scroll depth distribution
|
|
111
|
+
- Top pages by traffic
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## How the interest profile builds
|
|
116
|
+
|
|
117
|
+
The profile is entirely local — it never leaves the visitor's browser.
|
|
118
|
+
|
|
119
|
+
| Signal | Weight | Why |
|
|
120
|
+
|--------|--------|-----|
|
|
121
|
+
| Re-read (scrolled back) | 1.0 | Strongest signal — deliberate |
|
|
122
|
+
| Clipboard copy | 1.0 | User found it worth saving |
|
|
123
|
+
| Dwell > 5s | 0.5 | Moderate — attention held |
|
|
124
|
+
| Slow scroll velocity | 0.2 | Weak — ambiguous |
|
|
125
|
+
|
|
126
|
+
Keywords from engaged sections are weighted using an exponential moving average (EWMA). The top 120 keywords form the profile. No topic categories are pre-defined — the profile emerges from what each person reads.
|
|
127
|
+
|
|
128
|
+
Adaptation doesn't start until visit 2. The first visit is silent — building the profile — so there's no false adaptation before the system has signal.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Privacy model
|
|
133
|
+
|
|
134
|
+
| Data | Where it lives | Sent to server? |
|
|
135
|
+
|------|----------------|-----------------|
|
|
136
|
+
| Visitor interest profile | `localStorage` only | Never |
|
|
137
|
+
| Section dwell times | Analytics server (aggregate) | Yes — anonymous, no user identity |
|
|
138
|
+
| Scroll depth | Analytics server | Yes — anonymous |
|
|
139
|
+
| Page path | Server stores path only | Yes — no query string, no user linkage |
|
|
140
|
+
| IP address | Not recorded by Lens | Arrives as normal HTTP traffic |
|
|
141
|
+
| Device fingerprint | Never collected | — |
|
|
142
|
+
| Cross-site tracking | Impossible — `localStorage` is same-origin | — |
|
|
143
|
+
|
|
144
|
+
GDPR-compatible for analytics use without a consent banner (no personal data collected). If you enable visual adaptation, check your jurisdiction's requirements for disclosures about personalised content.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Build the npm package
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm run build # produces dist/lens.umd.js + dist/lens.esm.js
|
|
152
|
+
npm publish --dry-run # verify what's included
|
|
153
|
+
npm publish --access public
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Deploy to production
|
|
159
|
+
|
|
160
|
+
Standard Node.js/Express. Deploy to Railway, Render, Fly.io, or any VPS:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
PORT=3000 node server/index.js
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Then point `data-api` in your script tag to your deployed URL.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Roadmap
|
|
171
|
+
|
|
172
|
+
- [x] Section interest heatmap in dashboard
|
|
173
|
+
- [x] WordPress plugin
|
|
174
|
+
- [x] Multi-format demo (tech, news, magazine, tutorial)
|
|
175
|
+
- [x] npm package (`@zsolt/lens`)
|
|
176
|
+
- [ ] Multi-page interest graph (track topics across your whole publication)
|
|
177
|
+
- [ ] A/B mode — adapted vs. non-adapted to 50/50 of visitors, measure engagement delta
|
|
178
|
+
- [ ] Weekly publisher digest — "what your readers actually cared about this week"
|
|
179
|
+
- [ ] Ghost / Webflow plugins
|
|
180
|
+
- [ ] Optional AI-generated per-reader summary (publisher provides API key)
|
package/dist/lens.esm.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(f,s){"use strict";var x=s.currentScript,k=x.getAttribute("data-site-id")||"demo",T=(x.getAttribute("data-api")||"http://localhost:3001").replace(/\/$/,""),D=x.getAttribute("data-api-key")||null,_=x.getAttribute("data-mode")||"emphasis",v=x.getAttribute("data-debug")==="true",K=x.getAttribute("data-onboard")==="true",J=x.getAttribute("data-ab")==="true",C="lens_ab_v1";function W(){if(!J)return null;try{var e=JSON.parse(localStorage.getItem(C))||{};return e[k]||(e[k]=Math.random()<.5?"adapted":"control",localStorage.setItem(C,JSON.stringify(e))),e[k]}catch{return"adapted"}}var I=W(),N="lens_profile_v1";function A(){try{return JSON.parse(localStorage.getItem(N))||P()}catch{return P()}}function P(){return{topics:{},visits:0,lastSeen:null,style:{prefersDetail:.5,prefersTechnical:.5}}}function E(e){e.style||(e.style={prefersDetail:.5,prefersTechnical:.5}),e.style.prefersDetail==null&&(e.style.prefersDetail=.5),e.style.prefersTechnical==null&&(e.style.prefersTechnical=.5)}function j(e){try{localStorage.setItem(N,JSON.stringify(e))}catch{}}var R=["article",'[role="main"]',"main",".article-content",".post-content",".entry-content",".article-body","#content","#main-content","#article"];function G(){for(var e=0;e<R.length;e++)try{var n=s.querySelector(R[e]);if(n&&(n.innerText||"").length>400)return n}catch{}var r=null,i=0;return s.querySelectorAll("div, section").forEach(function(t){var l=Array.from(t.querySelectorAll("a")).reduce(function(u,g){return u+(g.innerText||"").length},0),a=(t.innerText||"").length-l*2;a>i&&a>300&&(i=a,r=t)}),r||s.body}function U(){var e=G(),n=e.querySelectorAll("p, h1, h2, h3, h4, h5, h6, li, blockquote"),r=[],i=0;return n.forEach(function(t){var l=(t.textContent||"").trim();if(!(l.length<25)){for(var a=t.parentElement;a;){var u=(a.tagName||"").toLowerCase();if(u==="nav"||u==="footer"||u==="header")return;a=a.parentElement}var g="lens-"+i++,y=l.substring(0,60).replace(/\s+/g," ");t.setAttribute("data-lens-id",g),t.setAttribute("data-lens-tag",t.tagName.toLowerCase()),t.setAttribute("data-lens-preview",y),r.push({id:g,el:t,text:l.substring(0,500),preview:y,score:.5})}}),r}var F=new Set(["about","after","again","also","another","because","been","before","being","between","both","came","come","could","does","doing","during","each","either","every","found","from","going","have","having","here","itself","just","like","make","many","might","more","most","much","must","never","next","none","nothing","often","only","other","over","same","should","since","some","such","than","that","their","them","then","there","these","they","this","those","through","time","under","until","very","well","were","what","when","where","which","while","will","with","within","would","your","into","upon","even","still","where","however","though","already","always","around","whether","without","simply","really","quite","rather","several","something","anything","everything","nothing","someone","anyone","everyone","first","second","third","last","next","previous","large","small","high","long","short","new","old","good","great","little","right","left","early","late","general","specific","different","similar","important","significant","particular","certain","possible","available","according","including","following","required","used","using","based","given","known","called","named","made","said","well","also","very"]);function L(e){return e.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(function(n){return n.length>4&&!F.has(n)})}function Q(e,n){var r=n.topics||{},i=Object.keys(r);if(i.length<3)return .5;var t=e.toLowerCase(),l=new Set(L(t)),a=0,u=0,g=0;if(i.forEach(function(o){var d=r[o];u+=d,l.has(o)&&(g+=d,a+=1)}),u===0)return .5;var y=g/Math.max(u*.15,.01);return Math.min(1,Math.max(0,y))}function z(){if(!s.getElementById("__lens_sdk_styles__")){var e=s.createElement("style");e.id="__lens_sdk_styles__",e.textContent='[data-lens-id]{transition:opacity .3s ease,border-left .3s ease}[data-lens-high]{border-left:3px solid rgba(99,102,241,.5)!important;padding-left:14px!important}[data-lens-low]{opacity:.45}.lens-collapsed{position:relative;max-height:3.8em;overflow:hidden}.lens-collapsed::after{content:"";position:absolute;bottom:0;left:0;right:0;height:36px;background:linear-gradient(transparent,rgba(255,255,255,.97));pointer-events:none}.lens-expand{display:inline-block;margin:3px 0 10px;font-size:12px;color:#6366f1;background:none;border:none;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:2px}.lens-expand:hover{color:#4f46e5}[data-lens-debug]::before{content:"Lens " attr(data-lens-debug);display:inline-block;font-size:9px;background:rgba(99,102,241,.12);color:#6366f1;border-radius:3px;padding:1px 6px;margin-right:6px;font-family:monospace;vertical-align:middle}.lens-rewrite-badge{display:inline-block;font-size:9px;background:rgba(16,185,129,.15);color:#059669;border-radius:3px;padding:1px 6px;margin-right:6px;font-family:monospace;vertical-align:middle}',s.head.appendChild(e)}}function S(e,n,r){if(_!=="off"&&!(Object.keys(n.topics).length<3)){E(n);var i=n.style.prefersDetail;z(),e.forEach(function(t){v&&t.el.setAttribute("data-lens-debug",Math.round(t.score*100)+"%"),t.score>=.55?t.el.setAttribute("data-lens-high",""):t.score<.25&&i>.65?t.el.setAttribute("data-lens-low",""):t.score<.25&&(_==="collapse"||_==="both")||t.score<.4&&i<.35&&_==="emphasis"?q(t.el,r):t.score<.25&&_==="emphasis"&&t.el.setAttribute("data-lens-low","")})}}function q(e,n){if(!((e.innerText||"").length<80)){e.classList.add("lens-collapsed");var r=s.createElement("button");r.className="lens-expand",r.textContent="Show more \u2193",r.addEventListener("click",function(){e.classList.remove("lens-collapsed"),r.remove(),n&&n.push({type:"expand",sectionId:e.getAttribute("data-lens-id"),words:(e.textContent||"").split(/\s+/).length})}),e.parentNode.insertBefore(r,e.nextSibling)}}function M(e,n){var r={},i={},t=new Set,l=n,a=[],u=f.scrollY,g=Date.now(),y=null,o=new IntersectionObserver(function(p){p.forEach(function(m){var c=m.target.getAttribute("data-lens-id");if(c){if(m.isIntersecting)r[c]=Date.now(),t.has(c)?l.push({type:"reread",sectionId:c,weight:1}):t.add(c);else if(r[c]){var b=(Date.now()-r[c])/1e3;i[c]=(i[c]||0)+b,delete r[c],b>2.5&&l.push({type:"dwell",sectionId:c,value:b,weight:.5})}}})},{threshold:.5});e.forEach(function(p){o.observe(p.el)}),f.addEventListener("scroll",function(){var p=Date.now(),m=Math.abs(f.scrollY-u),c=p-g;c>0&&m>0&&m/c<.35&&(clearTimeout(y),y=setTimeout(function(){for(var b=0;b<e.length;b++){var w=e[b].el.getBoundingClientRect();if(w.top>=0&&w.top<f.innerHeight*.5){l.push({type:"slowscroll",sectionId:e[b].id,weight:.2});break}}},500)),a.push({y:f.scrollY,t:p}),a.length>40&&a.shift(),u=f.scrollY,g=p},{passive:!0}),s.addEventListener("copy",function(){var p=f.getSelection();if(!(!p||!p.toString())){var m=p.toString().substring(0,30);e.forEach(function(c){c.el.textContent.indexOf(m)!==-1&&l.push({type:"copy",sectionId:c.id,weight:1})})}}),f.addEventListener("beforeunload",function(){Y(l,i,a,e)});var d;function h(){clearTimeout(d),d=setTimeout(function(){Y(l.splice(0),i,a,e)},9e4)}s.addEventListener("scroll",h,{passive:!0}),s.addEventListener("click",h,{passive:!0}),h()}var V=new Set(["api","sdk","latency","bandwidth","architecture","protocol","database","server","algorithm","runtime","cache","deploy","endpoint","throughput","inference","parameter","vector","embedding","quantization","transformer","kernel","compiler","binary","packet","token","benchmark","framework","library","repository","pipeline","cluster","container","kubernetes","docker"]);function Y(e,n,r,i){var t=A();E(t);var l=r.reduce(function(o,d){return Math.max(o,d.y)},0),a=Math.max(1,s.body.scrollHeight-f.innerHeight),u=Math.min(1,l/a);e.forEach(function(o){if(o.type==="expand"){t.style.prefersDetail=Math.min(1,t.style.prefersDetail+.15);return}if(!(o.weight<.5)){var d=i.find(function(h){return h.id===o.sectionId});d&&L(d.text).forEach(function(h){t.topics[h]=Math.min(1,(t.topics[h]||0)*.95+o.weight*.05)})}}),Object.keys(n).forEach(function(o){var d=n[o];if(!(d<4)){var h=i.find(function(w){return w.id===o});if(h){var p=Math.min(1,d/30)*.3;L(h.text).forEach(function(w){t.topics[w]=Math.min(1,(t.topics[w]||0)*.97+p*.03)});var m=h.text.split(/\s+/).length;m>120&&d>=10?t.style.prefersDetail=Math.min(1,t.style.prefersDetail+.04):m>120&&d<2&&(t.style.prefersDetail=Math.max(0,t.style.prefersDetail-.03));var c=L(h.text),b=c.filter(function(w){return V.has(w)}).length;b>=2&&d>=6&&(t.style.prefersTechnical=Math.min(1,t.style.prefersTechnical+.04))}}});var g=Object.entries(t.topics).sort(function(o,d){return d[1]-o[1]});t.topics=Object.fromEntries(g.slice(0,150)),t.visits+=1,t.lastSeen=Date.now(),j(t);var y=JSON.stringify({siteId:k,apiKey:D,path:f.location.pathname,scrollDepth:Math.round(u*100),sid:X(),abGroup:I,sections:i.map(function(o){return{id:o.id,tag:o.el.getAttribute("data-lens-tag"),preview:o.preview,dwell:Math.round(n[o.id]||0),score:Math.round(o.score*100),words:o.text.split(/\s+/).length}})});navigator.sendBeacon&&navigator.sendBeacon(T+"/events",y)}function X(){var e=sessionStorage.getItem("_lens_sid");return e||(e=Math.random().toString(36).substring(2,11),sessionStorage.setItem("_lens_sid",e)),e}function O(e){K&&(e.style._explicit||e.visits>1||setTimeout(function(){if(E(e),e.style._explicit)return;var n=s.createElement("div");n.id="__lens_toast__",n.style.cssText=["position:fixed;bottom:24px;left:50%;transform:translateX(-50%)","background:#1e293b;color:#e2e8f0;border-radius:10px","padding:14px 18px;font-family:system-ui,sans-serif;font-size:13px","box-shadow:0 8px 28px rgba(0,0,0,.35);z-index:999999","display:flex;align-items:center;gap:12px;max-width:340px"].join(";"),n.innerHTML=['<span style="flex:1;line-height:1.4">Prefer the quick summary or full detail?</span>','<button id="__lens_concise__" style="background:#6366f1;color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Quick</button>','<button id="__lens_detail__" style="background:#334155;color:#e2e8f0;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Full detail</button>','<button id="__lens_dismiss__" style="background:none;border:none;color:#64748b;cursor:pointer;font-size:16px;line-height:1;padding:0 2px" title="Dismiss">\xD7</button>'].join(""),s.body.appendChild(n);function r(i){var t=A();E(t),t.style.prefersDetail=i,t.style._explicit=!0,j(t),n.remove()}s.getElementById("__lens_concise__").addEventListener("click",function(){r(.2)}),s.getElementById("__lens_detail__").addEventListener("click",function(){r(.8)}),s.getElementById("__lens_dismiss__").addEventListener("click",function(){n.remove()})},6e4))}function $(e){var n=e.contentPreferences||{};return{topics:e.topics||{},visits:e.visits||0,lastSeen:Date.now(),style:{prefersDetail:n.prefersDetail!=null?n.prefersDetail:.5,prefersTechnical:n.prefersTechnical!=null?n.prefersTechnical:.5,prefersNarrative:n.prefersNarrative!=null?n.prefersNarrative:.5,prefersBullets:n.prefersBullets!=null?n.prefersBullets:.5}}}function Z(e,n){for(var r={},i=0;i<n.length;i++)r[n[i].id]=n[i];e.forEach(function(t){var l=r[t.id];if(l){var a=t.el;t.score=l.relevanceScore,a.style.transition="opacity 150ms ease",a.style.opacity="0",setTimeout(function(){if(a.innerHTML=l.content,a.setAttribute("data-lens-rewritten","true"),v){var u=s.createElement("span");u.className="lens-rewrite-badge",u.textContent="AI "+Math.round(l.relevanceScore*100)+"%",a.insertBefore(u,a.firstChild)}a.style.opacity="1"},160)}})}function ee(e,n,r){if(Object.keys(e.topics).length<3){v&&console.log("[Lens] rewrite: fewer than 3 topics \u2014 falling back to both mode"),S(n,e,r);return}if(!T){v&&console.log("[Lens] rewrite: no API_URL configured \u2014 falling back to both mode"),S(n,e,r);return}z();var i=JSON.stringify({siteId:k,path:f.location.pathname,sections:n.map(function(t){return{id:t.id,text:t.text}}),profile:{topics:e.topics,contentPreferences:e.style}});fetch(T+"/adapt",{method:"POST",headers:Object.assign({"content-type":"application/json"},D?{Authorization:"Bearer "+D}:{}),body:i}).then(function(t){if(t.status===503)return v&&console.log("[Lens] rewrite: server has no AI provider \u2014 falling back to both mode"),S(n,e,r),null;if(!t.ok)throw new Error("HTTP "+t.status);return t.json()}).then(function(t){if(t){if(!t.sections||!t.sections.length){v&&console.log("[Lens] rewrite: empty response \u2014 falling back to both mode"),S(n,e,r);return}v&&console.log("[Lens] rewrite: applying "+t.sections.length+" rewritten sections"),Z(n,t.sections)}}).catch(function(t){v&&console.warn("[Lens] rewrite error ("+t.message+") \u2014 falling back to both mode"),S(n,e,r)})}function B(e){E(e);var n=U();if(!(n.length<3)){n.forEach(function(i){i.score=Q(i.text,e)});var r=[];if(I==="control"){v&&console.log("[Lens] A/B group: control \u2014 tracking only, no adaptation"),M(n,r),O(e);return}if(v&&I==="adapted"&&console.log("[Lens] A/B group: adapted"),_==="rewrite"){let i=function(){setTimeout(function(){ee(e,n,r),M(n,r),O(e)},300)};s.readyState==="loading"?s.addEventListener("DOMContentLoaded",i):i()}else S(n,e,r),M(n,r),O(e);v&&(console.log("[Lens] "+n.length+" sections. Topics: "+Object.keys(e.topics).length+". Visits: "+e.visits+". Mode: "+_),console.log("[Lens] Style \u2014 prefersDetail: "+e.style.prefersDetail.toFixed(2)+", prefersTechnical: "+e.style.prefersTechnical.toFixed(2)),console.table(n.slice(0,10).map(function(i){return{id:i.id,score:Math.round(i.score*100)+"%",preview:i.preview}})))}}function H(){if(f.__lens__&&f.__lens__.isBrowser){f.__lens__.getProfile().then(function(e){B($(e))}).catch(function(){B(A())});return}B(A())}s.readyState==="loading"?s.addEventListener("DOMContentLoaded",H):H()})(window,document);
|
package/dist/lens.umd.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var Lens=(()=>{(function(f,s){"use strict";var x=s.currentScript,k=x.getAttribute("data-site-id")||"demo",T=(x.getAttribute("data-api")||"http://localhost:3001").replace(/\/$/,""),D=x.getAttribute("data-api-key")||null,_=x.getAttribute("data-mode")||"emphasis",v=x.getAttribute("data-debug")==="true",K=x.getAttribute("data-onboard")==="true",J=x.getAttribute("data-ab")==="true",C="lens_ab_v1";function W(){if(!J)return null;try{var e=JSON.parse(localStorage.getItem(C))||{};return e[k]||(e[k]=Math.random()<.5?"adapted":"control",localStorage.setItem(C,JSON.stringify(e))),e[k]}catch{return"adapted"}}var I=W(),N="lens_profile_v1";function A(){try{return JSON.parse(localStorage.getItem(N))||P()}catch{return P()}}function P(){return{topics:{},visits:0,lastSeen:null,style:{prefersDetail:.5,prefersTechnical:.5}}}function E(e){e.style||(e.style={prefersDetail:.5,prefersTechnical:.5}),e.style.prefersDetail==null&&(e.style.prefersDetail=.5),e.style.prefersTechnical==null&&(e.style.prefersTechnical=.5)}function j(e){try{localStorage.setItem(N,JSON.stringify(e))}catch{}}var R=["article",'[role="main"]',"main",".article-content",".post-content",".entry-content",".article-body","#content","#main-content","#article"];function G(){for(var e=0;e<R.length;e++)try{var n=s.querySelector(R[e]);if(n&&(n.innerText||"").length>400)return n}catch{}var r=null,i=0;return s.querySelectorAll("div, section").forEach(function(t){var l=Array.from(t.querySelectorAll("a")).reduce(function(u,g){return u+(g.innerText||"").length},0),a=(t.innerText||"").length-l*2;a>i&&a>300&&(i=a,r=t)}),r||s.body}function U(){var e=G(),n=e.querySelectorAll("p, h1, h2, h3, h4, h5, h6, li, blockquote"),r=[],i=0;return n.forEach(function(t){var l=(t.textContent||"").trim();if(!(l.length<25)){for(var a=t.parentElement;a;){var u=(a.tagName||"").toLowerCase();if(u==="nav"||u==="footer"||u==="header")return;a=a.parentElement}var g="lens-"+i++,y=l.substring(0,60).replace(/\s+/g," ");t.setAttribute("data-lens-id",g),t.setAttribute("data-lens-tag",t.tagName.toLowerCase()),t.setAttribute("data-lens-preview",y),r.push({id:g,el:t,text:l.substring(0,500),preview:y,score:.5})}}),r}var F=new Set(["about","after","again","also","another","because","been","before","being","between","both","came","come","could","does","doing","during","each","either","every","found","from","going","have","having","here","itself","just","like","make","many","might","more","most","much","must","never","next","none","nothing","often","only","other","over","same","should","since","some","such","than","that","their","them","then","there","these","they","this","those","through","time","under","until","very","well","were","what","when","where","which","while","will","with","within","would","your","into","upon","even","still","where","however","though","already","always","around","whether","without","simply","really","quite","rather","several","something","anything","everything","nothing","someone","anyone","everyone","first","second","third","last","next","previous","large","small","high","long","short","new","old","good","great","little","right","left","early","late","general","specific","different","similar","important","significant","particular","certain","possible","available","according","including","following","required","used","using","based","given","known","called","named","made","said","well","also","very"]);function L(e){return e.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(function(n){return n.length>4&&!F.has(n)})}function Q(e,n){var r=n.topics||{},i=Object.keys(r);if(i.length<3)return .5;var t=e.toLowerCase(),l=new Set(L(t)),a=0,u=0,g=0;if(i.forEach(function(o){var d=r[o];u+=d,l.has(o)&&(g+=d,a+=1)}),u===0)return .5;var y=g/Math.max(u*.15,.01);return Math.min(1,Math.max(0,y))}function z(){if(!s.getElementById("__lens_sdk_styles__")){var e=s.createElement("style");e.id="__lens_sdk_styles__",e.textContent='[data-lens-id]{transition:opacity .3s ease,border-left .3s ease}[data-lens-high]{border-left:3px solid rgba(99,102,241,.5)!important;padding-left:14px!important}[data-lens-low]{opacity:.45}.lens-collapsed{position:relative;max-height:3.8em;overflow:hidden}.lens-collapsed::after{content:"";position:absolute;bottom:0;left:0;right:0;height:36px;background:linear-gradient(transparent,rgba(255,255,255,.97));pointer-events:none}.lens-expand{display:inline-block;margin:3px 0 10px;font-size:12px;color:#6366f1;background:none;border:none;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:2px}.lens-expand:hover{color:#4f46e5}[data-lens-debug]::before{content:"Lens " attr(data-lens-debug);display:inline-block;font-size:9px;background:rgba(99,102,241,.12);color:#6366f1;border-radius:3px;padding:1px 6px;margin-right:6px;font-family:monospace;vertical-align:middle}.lens-rewrite-badge{display:inline-block;font-size:9px;background:rgba(16,185,129,.15);color:#059669;border-radius:3px;padding:1px 6px;margin-right:6px;font-family:monospace;vertical-align:middle}',s.head.appendChild(e)}}function S(e,n,r){if(_!=="off"&&!(Object.keys(n.topics).length<3)){E(n);var i=n.style.prefersDetail;z(),e.forEach(function(t){v&&t.el.setAttribute("data-lens-debug",Math.round(t.score*100)+"%"),t.score>=.55?t.el.setAttribute("data-lens-high",""):t.score<.25&&i>.65?t.el.setAttribute("data-lens-low",""):t.score<.25&&(_==="collapse"||_==="both")||t.score<.4&&i<.35&&_==="emphasis"?q(t.el,r):t.score<.25&&_==="emphasis"&&t.el.setAttribute("data-lens-low","")})}}function q(e,n){if(!((e.innerText||"").length<80)){e.classList.add("lens-collapsed");var r=s.createElement("button");r.className="lens-expand",r.textContent="Show more \u2193",r.addEventListener("click",function(){e.classList.remove("lens-collapsed"),r.remove(),n&&n.push({type:"expand",sectionId:e.getAttribute("data-lens-id"),words:(e.textContent||"").split(/\s+/).length})}),e.parentNode.insertBefore(r,e.nextSibling)}}function M(e,n){var r={},i={},t=new Set,l=n,a=[],u=f.scrollY,g=Date.now(),y=null,o=new IntersectionObserver(function(p){p.forEach(function(m){var c=m.target.getAttribute("data-lens-id");if(c){if(m.isIntersecting)r[c]=Date.now(),t.has(c)?l.push({type:"reread",sectionId:c,weight:1}):t.add(c);else if(r[c]){var b=(Date.now()-r[c])/1e3;i[c]=(i[c]||0)+b,delete r[c],b>2.5&&l.push({type:"dwell",sectionId:c,value:b,weight:.5})}}})},{threshold:.5});e.forEach(function(p){o.observe(p.el)}),f.addEventListener("scroll",function(){var p=Date.now(),m=Math.abs(f.scrollY-u),c=p-g;c>0&&m>0&&m/c<.35&&(clearTimeout(y),y=setTimeout(function(){for(var b=0;b<e.length;b++){var w=e[b].el.getBoundingClientRect();if(w.top>=0&&w.top<f.innerHeight*.5){l.push({type:"slowscroll",sectionId:e[b].id,weight:.2});break}}},500)),a.push({y:f.scrollY,t:p}),a.length>40&&a.shift(),u=f.scrollY,g=p},{passive:!0}),s.addEventListener("copy",function(){var p=f.getSelection();if(!(!p||!p.toString())){var m=p.toString().substring(0,30);e.forEach(function(c){c.el.textContent.indexOf(m)!==-1&&l.push({type:"copy",sectionId:c.id,weight:1})})}}),f.addEventListener("beforeunload",function(){Y(l,i,a,e)});var d;function h(){clearTimeout(d),d=setTimeout(function(){Y(l.splice(0),i,a,e)},9e4)}s.addEventListener("scroll",h,{passive:!0}),s.addEventListener("click",h,{passive:!0}),h()}var V=new Set(["api","sdk","latency","bandwidth","architecture","protocol","database","server","algorithm","runtime","cache","deploy","endpoint","throughput","inference","parameter","vector","embedding","quantization","transformer","kernel","compiler","binary","packet","token","benchmark","framework","library","repository","pipeline","cluster","container","kubernetes","docker"]);function Y(e,n,r,i){var t=A();E(t);var l=r.reduce(function(o,d){return Math.max(o,d.y)},0),a=Math.max(1,s.body.scrollHeight-f.innerHeight),u=Math.min(1,l/a);e.forEach(function(o){if(o.type==="expand"){t.style.prefersDetail=Math.min(1,t.style.prefersDetail+.15);return}if(!(o.weight<.5)){var d=i.find(function(h){return h.id===o.sectionId});d&&L(d.text).forEach(function(h){t.topics[h]=Math.min(1,(t.topics[h]||0)*.95+o.weight*.05)})}}),Object.keys(n).forEach(function(o){var d=n[o];if(!(d<4)){var h=i.find(function(w){return w.id===o});if(h){var p=Math.min(1,d/30)*.3;L(h.text).forEach(function(w){t.topics[w]=Math.min(1,(t.topics[w]||0)*.97+p*.03)});var m=h.text.split(/\s+/).length;m>120&&d>=10?t.style.prefersDetail=Math.min(1,t.style.prefersDetail+.04):m>120&&d<2&&(t.style.prefersDetail=Math.max(0,t.style.prefersDetail-.03));var c=L(h.text),b=c.filter(function(w){return V.has(w)}).length;b>=2&&d>=6&&(t.style.prefersTechnical=Math.min(1,t.style.prefersTechnical+.04))}}});var g=Object.entries(t.topics).sort(function(o,d){return d[1]-o[1]});t.topics=Object.fromEntries(g.slice(0,150)),t.visits+=1,t.lastSeen=Date.now(),j(t);var y=JSON.stringify({siteId:k,apiKey:D,path:f.location.pathname,scrollDepth:Math.round(u*100),sid:X(),abGroup:I,sections:i.map(function(o){return{id:o.id,tag:o.el.getAttribute("data-lens-tag"),preview:o.preview,dwell:Math.round(n[o.id]||0),score:Math.round(o.score*100),words:o.text.split(/\s+/).length}})});navigator.sendBeacon&&navigator.sendBeacon(T+"/events",y)}function X(){var e=sessionStorage.getItem("_lens_sid");return e||(e=Math.random().toString(36).substring(2,11),sessionStorage.setItem("_lens_sid",e)),e}function O(e){K&&(e.style._explicit||e.visits>1||setTimeout(function(){if(E(e),e.style._explicit)return;var n=s.createElement("div");n.id="__lens_toast__",n.style.cssText=["position:fixed;bottom:24px;left:50%;transform:translateX(-50%)","background:#1e293b;color:#e2e8f0;border-radius:10px","padding:14px 18px;font-family:system-ui,sans-serif;font-size:13px","box-shadow:0 8px 28px rgba(0,0,0,.35);z-index:999999","display:flex;align-items:center;gap:12px;max-width:340px"].join(";"),n.innerHTML=['<span style="flex:1;line-height:1.4">Prefer the quick summary or full detail?</span>','<button id="__lens_concise__" style="background:#6366f1;color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Quick</button>','<button id="__lens_detail__" style="background:#334155;color:#e2e8f0;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Full detail</button>','<button id="__lens_dismiss__" style="background:none;border:none;color:#64748b;cursor:pointer;font-size:16px;line-height:1;padding:0 2px" title="Dismiss">\xD7</button>'].join(""),s.body.appendChild(n);function r(i){var t=A();E(t),t.style.prefersDetail=i,t.style._explicit=!0,j(t),n.remove()}s.getElementById("__lens_concise__").addEventListener("click",function(){r(.2)}),s.getElementById("__lens_detail__").addEventListener("click",function(){r(.8)}),s.getElementById("__lens_dismiss__").addEventListener("click",function(){n.remove()})},6e4))}function $(e){var n=e.contentPreferences||{};return{topics:e.topics||{},visits:e.visits||0,lastSeen:Date.now(),style:{prefersDetail:n.prefersDetail!=null?n.prefersDetail:.5,prefersTechnical:n.prefersTechnical!=null?n.prefersTechnical:.5,prefersNarrative:n.prefersNarrative!=null?n.prefersNarrative:.5,prefersBullets:n.prefersBullets!=null?n.prefersBullets:.5}}}function Z(e,n){for(var r={},i=0;i<n.length;i++)r[n[i].id]=n[i];e.forEach(function(t){var l=r[t.id];if(l){var a=t.el;t.score=l.relevanceScore,a.style.transition="opacity 150ms ease",a.style.opacity="0",setTimeout(function(){if(a.innerHTML=l.content,a.setAttribute("data-lens-rewritten","true"),v){var u=s.createElement("span");u.className="lens-rewrite-badge",u.textContent="AI "+Math.round(l.relevanceScore*100)+"%",a.insertBefore(u,a.firstChild)}a.style.opacity="1"},160)}})}function ee(e,n,r){if(Object.keys(e.topics).length<3){v&&console.log("[Lens] rewrite: fewer than 3 topics \u2014 falling back to both mode"),S(n,e,r);return}if(!T){v&&console.log("[Lens] rewrite: no API_URL configured \u2014 falling back to both mode"),S(n,e,r);return}z();var i=JSON.stringify({siteId:k,path:f.location.pathname,sections:n.map(function(t){return{id:t.id,text:t.text}}),profile:{topics:e.topics,contentPreferences:e.style}});fetch(T+"/adapt",{method:"POST",headers:Object.assign({"content-type":"application/json"},D?{Authorization:"Bearer "+D}:{}),body:i}).then(function(t){if(t.status===503)return v&&console.log("[Lens] rewrite: server has no AI provider \u2014 falling back to both mode"),S(n,e,r),null;if(!t.ok)throw new Error("HTTP "+t.status);return t.json()}).then(function(t){if(t){if(!t.sections||!t.sections.length){v&&console.log("[Lens] rewrite: empty response \u2014 falling back to both mode"),S(n,e,r);return}v&&console.log("[Lens] rewrite: applying "+t.sections.length+" rewritten sections"),Z(n,t.sections)}}).catch(function(t){v&&console.warn("[Lens] rewrite error ("+t.message+") \u2014 falling back to both mode"),S(n,e,r)})}function B(e){E(e);var n=U();if(!(n.length<3)){n.forEach(function(i){i.score=Q(i.text,e)});var r=[];if(I==="control"){v&&console.log("[Lens] A/B group: control \u2014 tracking only, no adaptation"),M(n,r),O(e);return}if(v&&I==="adapted"&&console.log("[Lens] A/B group: adapted"),_==="rewrite"){let i=function(){setTimeout(function(){ee(e,n,r),M(n,r),O(e)},300)};s.readyState==="loading"?s.addEventListener("DOMContentLoaded",i):i()}else S(n,e,r),M(n,r),O(e);v&&(console.log("[Lens] "+n.length+" sections. Topics: "+Object.keys(e.topics).length+". Visits: "+e.visits+". Mode: "+_),console.log("[Lens] Style \u2014 prefersDetail: "+e.style.prefersDetail.toFixed(2)+", prefersTechnical: "+e.style.prefersTechnical.toFixed(2)),console.table(n.slice(0,10).map(function(i){return{id:i.id,score:Math.round(i.score*100)+"%",preview:i.preview}})))}}function H(){if(f.__lens__&&f.__lens__.isBrowser){f.__lens__.getProfile().then(function(e){B($(e))}).catch(function(){B(A())});return}B(A())}s.readyState==="loading"?s.addEventListener("DOMContentLoaded",H):H()})(window,document);})();
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zsoltcsaszti/lens",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Reader intelligence for publishers. One script tag. Zero tracking cookies.",
|
|
5
|
+
"main": "dist/lens.umd.js",
|
|
6
|
+
"module": "dist/lens.esm.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/lens.esm.js",
|
|
10
|
+
"require": "./dist/lens.umd.js",
|
|
11
|
+
"default": "./dist/lens.umd.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"sdk/lens.js",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": true,
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/zsoltcsaszti/lens-sdk"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"analytics",
|
|
27
|
+
"publisher",
|
|
28
|
+
"reading",
|
|
29
|
+
"engagement",
|
|
30
|
+
"privacy",
|
|
31
|
+
"reader-intelligence"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node build.js",
|
|
35
|
+
"start": "node server/index.js",
|
|
36
|
+
"dev": "node --watch server/index.js",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"esbuild": "^0.21.5"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"cors": "^2.8.5",
|
|
44
|
+
"express": "^4.19.2"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/sdk/lens.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens SDK v0.2
|
|
3
|
+
* Drop-in reading intelligence for any publisher.
|
|
4
|
+
*
|
|
5
|
+
* <script src="lens.js"
|
|
6
|
+
* data-site-id="your_site_id"
|
|
7
|
+
* data-api="https://your-lens-server.com"
|
|
8
|
+
* data-mode="emphasis" <!-- emphasis | collapse | both | rewrite | off -->
|
|
9
|
+
* data-ab="true"> <!-- enable A/B test: 50% adapted, 50% control -->
|
|
10
|
+
* </script>
|
|
11
|
+
*/
|
|
12
|
+
(function (w, d) {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// ── Config ─────────────────────────────────────────────────────────────
|
|
16
|
+
var script = d.currentScript;
|
|
17
|
+
var SITE_ID = script.getAttribute('data-site-id') || 'demo';
|
|
18
|
+
var API_URL = (script.getAttribute('data-api') || 'http://localhost:3001').replace(/\/$/, '');
|
|
19
|
+
var API_KEY = script.getAttribute('data-api-key') || null;
|
|
20
|
+
var MODE = script.getAttribute('data-mode') || 'emphasis';
|
|
21
|
+
var DEBUG = script.getAttribute('data-debug') === 'true';
|
|
22
|
+
var ONBOARD = script.getAttribute('data-onboard') === 'true';
|
|
23
|
+
var AB = script.getAttribute('data-ab') === 'true';
|
|
24
|
+
|
|
25
|
+
// ── A/B assignment ──────────────────────────────────────────────────────
|
|
26
|
+
var AB_STORE_KEY = 'lens_ab_v1';
|
|
27
|
+
|
|
28
|
+
function getAbGroup() {
|
|
29
|
+
if (!AB) return null;
|
|
30
|
+
try {
|
|
31
|
+
var store = JSON.parse(localStorage.getItem(AB_STORE_KEY)) || {};
|
|
32
|
+
if (!store[SITE_ID]) {
|
|
33
|
+
store[SITE_ID] = Math.random() < 0.5 ? 'adapted' : 'control';
|
|
34
|
+
localStorage.setItem(AB_STORE_KEY, JSON.stringify(store));
|
|
35
|
+
}
|
|
36
|
+
return store[SITE_ID];
|
|
37
|
+
} catch (_) { return 'adapted'; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var AB_GROUP = getAbGroup(); // sticky for this page session
|
|
41
|
+
|
|
42
|
+
// ── Local profile ───────────────────────────────────────────────────────
|
|
43
|
+
var STORE_KEY = 'lens_profile_v1';
|
|
44
|
+
|
|
45
|
+
function loadProfile() {
|
|
46
|
+
try { return JSON.parse(localStorage.getItem(STORE_KEY)) || freshProfile(); }
|
|
47
|
+
catch (_) { return freshProfile(); }
|
|
48
|
+
}
|
|
49
|
+
function freshProfile() {
|
|
50
|
+
return { topics: {}, visits: 0, lastSeen: null, style: { prefersDetail: 0.5, prefersTechnical: 0.5 } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureStyle(p) {
|
|
54
|
+
if (!p.style) p.style = { prefersDetail: 0.5, prefersTechnical: 0.5 };
|
|
55
|
+
if (p.style.prefersDetail == null) p.style.prefersDetail = 0.5;
|
|
56
|
+
if (p.style.prefersTechnical == null) p.style.prefersTechnical = 0.5;
|
|
57
|
+
}
|
|
58
|
+
function saveProfile(p) {
|
|
59
|
+
try { localStorage.setItem(STORE_KEY, JSON.stringify(p)); } catch (_) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Section detection ───────────────────────────────────────────────────
|
|
63
|
+
var AREA_SELECTORS = [
|
|
64
|
+
'article', '[role="main"]', 'main',
|
|
65
|
+
'.article-content', '.post-content', '.entry-content', '.article-body',
|
|
66
|
+
'#content', '#main-content', '#article'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function findContentArea() {
|
|
70
|
+
for (var i = 0; i < AREA_SELECTORS.length; i++) {
|
|
71
|
+
try {
|
|
72
|
+
var el = d.querySelector(AREA_SELECTORS[i]);
|
|
73
|
+
if (el && (el.innerText || '').length > 400) return el;
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
var best = null, bestScore = 0;
|
|
77
|
+
d.querySelectorAll('div, section').forEach(function (el) {
|
|
78
|
+
var linkLen = Array.from(el.querySelectorAll('a'))
|
|
79
|
+
.reduce(function (s, a) { return s + (a.innerText || '').length; }, 0);
|
|
80
|
+
var score = (el.innerText || '').length - linkLen * 2;
|
|
81
|
+
if (score > bestScore && score > 300) { bestScore = score; best = el; }
|
|
82
|
+
});
|
|
83
|
+
return best || d.body;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectSections() {
|
|
87
|
+
var area = findContentArea();
|
|
88
|
+
var elements = area.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, blockquote');
|
|
89
|
+
var sections = [];
|
|
90
|
+
var idx = 0;
|
|
91
|
+
|
|
92
|
+
elements.forEach(function (el) {
|
|
93
|
+
var text = (el.textContent || '').trim();
|
|
94
|
+
if (text.length < 25) return;
|
|
95
|
+
|
|
96
|
+
// Skip nav/header/footer parents
|
|
97
|
+
var p = el.parentElement;
|
|
98
|
+
while (p) {
|
|
99
|
+
var tag = (p.tagName || '').toLowerCase();
|
|
100
|
+
if (tag === 'nav' || tag === 'footer' || tag === 'header') return;
|
|
101
|
+
p = p.parentElement;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var id = 'lens-' + idx++;
|
|
105
|
+
var preview = text.substring(0, 60).replace(/\s+/g, ' '); // human-readable label
|
|
106
|
+
el.setAttribute('data-lens-id', id);
|
|
107
|
+
el.setAttribute('data-lens-tag', el.tagName.toLowerCase());
|
|
108
|
+
el.setAttribute('data-lens-preview', preview);
|
|
109
|
+
|
|
110
|
+
sections.push({ id: id, el: el, text: text.substring(0, 500), preview: preview, score: 0.5 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return sections;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Stopwords ───────────────────────────────────────────────────────────
|
|
117
|
+
var STOPWORDS = new Set([
|
|
118
|
+
'about','after','again','also','another','because','been','before','being',
|
|
119
|
+
'between','both','came','come','could','does','doing','during','each',
|
|
120
|
+
'either','every','found','from','going','have','having','here','itself',
|
|
121
|
+
'just','like','make','many','might','more','most','much','must','never',
|
|
122
|
+
'next','none','nothing','often','only','other','over','same','should',
|
|
123
|
+
'since','some','such','than','that','their','them','then','there','these',
|
|
124
|
+
'they','this','those','through','time','under','until','very','well',
|
|
125
|
+
'were','what','when','where','which','while','will','with','within',
|
|
126
|
+
'would','your','into','upon','even','still','where','however','though',
|
|
127
|
+
'already','always','around','whether','without','simply','really','quite',
|
|
128
|
+
'rather','several','something','anything','everything','nothing','someone',
|
|
129
|
+
'anyone','everyone','first','second','third','last','next','previous',
|
|
130
|
+
'large','small','high','long','short','new','old','good','great','little',
|
|
131
|
+
'right','left','early','late','general','specific','different','similar',
|
|
132
|
+
'important','significant','particular','certain','possible','available',
|
|
133
|
+
'according','including','following','required','used','using','based',
|
|
134
|
+
'given','known','called','named','made','said','well','also','very'
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
function extractKeywords(text) {
|
|
138
|
+
return text
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
141
|
+
.split(/\s+/)
|
|
142
|
+
.filter(function (w) { return w.length > 4 && !STOPWORDS.has(w); });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Scoring ─────────────────────────────────────────────────────────────
|
|
146
|
+
// Lower the minimum topics threshold — adapt sooner, improve over time
|
|
147
|
+
function scoreSection(text, profile) {
|
|
148
|
+
var topics = profile.topics || {};
|
|
149
|
+
var topicKeys = Object.keys(topics);
|
|
150
|
+
if (topicKeys.length < 3) return 0.5; // truly no data yet
|
|
151
|
+
|
|
152
|
+
var lower = text.toLowerCase();
|
|
153
|
+
var sectionKws = new Set(extractKeywords(lower));
|
|
154
|
+
var matched = 0;
|
|
155
|
+
var totalW = 0;
|
|
156
|
+
var matchW = 0;
|
|
157
|
+
|
|
158
|
+
topicKeys.forEach(function (kw) {
|
|
159
|
+
var w = topics[kw];
|
|
160
|
+
totalW += w;
|
|
161
|
+
if (sectionKws.has(kw)) {
|
|
162
|
+
matchW += w;
|
|
163
|
+
matched += 1;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (totalW === 0) return 0.5;
|
|
168
|
+
|
|
169
|
+
// Weighted overlap ratio — how much of the user's interest weight is present
|
|
170
|
+
var raw = matchW / Math.max(totalW * 0.15, 0.01);
|
|
171
|
+
return Math.min(1, Math.max(0, raw));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── CSS adaptation (never touches text) ────────────────────────────────
|
|
175
|
+
function injectStyles() {
|
|
176
|
+
if (d.getElementById('__lens_sdk_styles__')) return;
|
|
177
|
+
var s = d.createElement('style');
|
|
178
|
+
s.id = '__lens_sdk_styles__';
|
|
179
|
+
s.textContent =
|
|
180
|
+
'[data-lens-id]{transition:opacity .3s ease,border-left .3s ease}' +
|
|
181
|
+
'[data-lens-high]{border-left:3px solid rgba(99,102,241,.5)!important;padding-left:14px!important}' +
|
|
182
|
+
'[data-lens-low]{opacity:.45}' +
|
|
183
|
+
'.lens-collapsed{position:relative;max-height:3.8em;overflow:hidden}' +
|
|
184
|
+
'.lens-collapsed::after{content:"";position:absolute;bottom:0;left:0;right:0;height:36px;' +
|
|
185
|
+
'background:linear-gradient(transparent,rgba(255,255,255,.97));pointer-events:none}' +
|
|
186
|
+
'.lens-expand{display:inline-block;margin:3px 0 10px;font-size:12px;color:#6366f1;' +
|
|
187
|
+
'background:none;border:none;cursor:pointer;padding:0;text-decoration:underline;' +
|
|
188
|
+
'text-underline-offset:2px}' +
|
|
189
|
+
'.lens-expand:hover{color:#4f46e5}' +
|
|
190
|
+
'[data-lens-debug]::before{content:"Lens " attr(data-lens-debug);display:inline-block;' +
|
|
191
|
+
'font-size:9px;background:rgba(99,102,241,.12);color:#6366f1;border-radius:3px;' +
|
|
192
|
+
'padding:1px 6px;margin-right:6px;font-family:monospace;vertical-align:middle}' +
|
|
193
|
+
'.lens-rewrite-badge{display:inline-block;font-size:9px;background:rgba(16,185,129,.15);' +
|
|
194
|
+
'color:#059669;border-radius:3px;padding:1px 6px;margin-right:6px;' +
|
|
195
|
+
'font-family:monospace;vertical-align:middle}';
|
|
196
|
+
d.head.appendChild(s);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function adapt(sections, profile, expandEvents) {
|
|
200
|
+
if (MODE === 'off') return;
|
|
201
|
+
if (Object.keys(profile.topics).length < 3) return;
|
|
202
|
+
|
|
203
|
+
ensureStyle(profile);
|
|
204
|
+
var detail = profile.style.prefersDetail;
|
|
205
|
+
|
|
206
|
+
injectStyles();
|
|
207
|
+
|
|
208
|
+
sections.forEach(function (s) {
|
|
209
|
+
if (DEBUG) s.el.setAttribute('data-lens-debug', Math.round(s.score * 100) + '%');
|
|
210
|
+
|
|
211
|
+
if (s.score >= 0.55) {
|
|
212
|
+
s.el.setAttribute('data-lens-high', '');
|
|
213
|
+
} else if (s.score < 0.25 && detail > 0.65) {
|
|
214
|
+
// Detail-preferring reader: de-emphasise rather than collapse
|
|
215
|
+
s.el.setAttribute('data-lens-low', '');
|
|
216
|
+
} else if (s.score < 0.25 && (MODE === 'collapse' || MODE === 'both')) {
|
|
217
|
+
collapseSection(s.el, expandEvents);
|
|
218
|
+
} else if (s.score < 0.4 && detail < 0.35 && MODE === 'emphasis') {
|
|
219
|
+
// Concise-preferring reader: collapse even in emphasis mode for low-relevance sections
|
|
220
|
+
collapseSection(s.el, expandEvents);
|
|
221
|
+
} else if (s.score < 0.25 && MODE === 'emphasis') {
|
|
222
|
+
s.el.setAttribute('data-lens-low', '');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function collapseSection(el, expandEvents) {
|
|
228
|
+
if ((el.innerText || '').length < 80) return;
|
|
229
|
+
el.classList.add('lens-collapsed');
|
|
230
|
+
var btn = d.createElement('button');
|
|
231
|
+
btn.className = 'lens-expand';
|
|
232
|
+
btn.textContent = 'Show more ↓';
|
|
233
|
+
btn.addEventListener('click', function () {
|
|
234
|
+
el.classList.remove('lens-collapsed');
|
|
235
|
+
btn.remove();
|
|
236
|
+
if (expandEvents) {
|
|
237
|
+
expandEvents.push({ type: 'expand', sectionId: el.getAttribute('data-lens-id'), words: (el.textContent || '').split(/\s+/).length });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
el.parentNode.insertBefore(btn, el.nextSibling);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Behavior tracking ───────────────────────────────────────────────────
|
|
244
|
+
function startTracking(sections, expandEvents) {
|
|
245
|
+
var dwellStart = {};
|
|
246
|
+
var dwellTotal = {};
|
|
247
|
+
var seen = new Set();
|
|
248
|
+
var events = expandEvents; // shared array — expand clicks land here too
|
|
249
|
+
var scrollLog = [];
|
|
250
|
+
var lastY = w.scrollY;
|
|
251
|
+
var lastT = Date.now();
|
|
252
|
+
var slowScrollTimer = null; // debounce slow-scroll signal
|
|
253
|
+
|
|
254
|
+
// IntersectionObserver: dwell + re-reads
|
|
255
|
+
var io = new IntersectionObserver(function (entries) {
|
|
256
|
+
entries.forEach(function (e) {
|
|
257
|
+
var id = e.target.getAttribute('data-lens-id');
|
|
258
|
+
if (!id) return;
|
|
259
|
+
|
|
260
|
+
if (e.isIntersecting) {
|
|
261
|
+
dwellStart[id] = Date.now();
|
|
262
|
+
if (seen.has(id)) {
|
|
263
|
+
events.push({ type: 'reread', sectionId: id, weight: 1.0 });
|
|
264
|
+
} else {
|
|
265
|
+
seen.add(id);
|
|
266
|
+
}
|
|
267
|
+
} else if (dwellStart[id]) {
|
|
268
|
+
var dwell = (Date.now() - dwellStart[id]) / 1000;
|
|
269
|
+
dwellTotal[id] = (dwellTotal[id] || 0) + dwell;
|
|
270
|
+
delete dwellStart[id];
|
|
271
|
+
if (dwell > 2.5) {
|
|
272
|
+
events.push({ type: 'dwell', sectionId: id, value: dwell, weight: 0.5 });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}, { threshold: 0.5 });
|
|
277
|
+
|
|
278
|
+
sections.forEach(function (s) { io.observe(s.el); });
|
|
279
|
+
|
|
280
|
+
// Scroll velocity — debounced to fire at most once per 500ms
|
|
281
|
+
w.addEventListener('scroll', function () {
|
|
282
|
+
var now = Date.now();
|
|
283
|
+
var dy = Math.abs(w.scrollY - lastY);
|
|
284
|
+
var dt = now - lastT;
|
|
285
|
+
|
|
286
|
+
if (dt > 0 && dy > 0 && dy / dt < 0.35) {
|
|
287
|
+
clearTimeout(slowScrollTimer);
|
|
288
|
+
slowScrollTimer = setTimeout(function () {
|
|
289
|
+
// Find the topmost visible section at the moment of slow scroll
|
|
290
|
+
for (var i = 0; i < sections.length; i++) {
|
|
291
|
+
var r = sections[i].el.getBoundingClientRect();
|
|
292
|
+
if (r.top >= 0 && r.top < w.innerHeight * 0.5) {
|
|
293
|
+
events.push({ type: 'slowscroll', sectionId: sections[i].id, weight: 0.2 });
|
|
294
|
+
break; // only one signal per slow-scroll event
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}, 500);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
scrollLog.push({ y: w.scrollY, t: now });
|
|
301
|
+
if (scrollLog.length > 40) scrollLog.shift();
|
|
302
|
+
lastY = w.scrollY;
|
|
303
|
+
lastT = now;
|
|
304
|
+
}, { passive: true });
|
|
305
|
+
|
|
306
|
+
// Clipboard copy
|
|
307
|
+
d.addEventListener('copy', function () {
|
|
308
|
+
var sel = w.getSelection();
|
|
309
|
+
if (!sel || !sel.toString()) return;
|
|
310
|
+
var prefix = sel.toString().substring(0, 30);
|
|
311
|
+
sections.forEach(function (s) {
|
|
312
|
+
if (s.el.textContent.indexOf(prefix) !== -1) {
|
|
313
|
+
events.push({ type: 'copy', sectionId: s.id, weight: 1.0 });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Page leave
|
|
319
|
+
w.addEventListener('beforeunload', function () {
|
|
320
|
+
flush(events, dwellTotal, scrollLog, sections);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Flush after 90s idle
|
|
324
|
+
var idleTimer;
|
|
325
|
+
function resetIdle() {
|
|
326
|
+
clearTimeout(idleTimer);
|
|
327
|
+
idleTimer = setTimeout(function () {
|
|
328
|
+
flush(events.splice(0), dwellTotal, scrollLog, sections);
|
|
329
|
+
}, 90000);
|
|
330
|
+
}
|
|
331
|
+
d.addEventListener('scroll', resetIdle, { passive: true });
|
|
332
|
+
d.addEventListener('click', resetIdle, { passive: true });
|
|
333
|
+
resetIdle();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
var TECHNICAL_WORDS = new Set([
|
|
337
|
+
'api','sdk','latency','bandwidth','architecture','protocol','database','server',
|
|
338
|
+
'algorithm','runtime','cache','deploy','endpoint','throughput','inference',
|
|
339
|
+
'parameter','vector','embedding','quantization','transformer','kernel',
|
|
340
|
+
'compiler','binary','packet','token','benchmark','framework','library',
|
|
341
|
+
'repository','pipeline','cluster','container','kubernetes','docker'
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
// ── Flush: update profile + report to server ────────────────────────────
|
|
345
|
+
function flush(events, dwellTotal, scrollLog, sections) {
|
|
346
|
+
var profile = loadProfile();
|
|
347
|
+
ensureStyle(profile);
|
|
348
|
+
var maxY = scrollLog.reduce(function (m, p) { return Math.max(m, p.y); }, 0);
|
|
349
|
+
var docH = Math.max(1, d.body.scrollHeight - w.innerHeight);
|
|
350
|
+
var scrollDepth = Math.min(1, maxY / docH);
|
|
351
|
+
|
|
352
|
+
// High-signal events → extract keywords from engaged sections
|
|
353
|
+
events.forEach(function (ev) {
|
|
354
|
+
if (ev.type === 'expand') {
|
|
355
|
+
// Reader explicitly expanded a collapsed section → strong detail signal
|
|
356
|
+
profile.style.prefersDetail = Math.min(1, profile.style.prefersDetail + 0.15);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (ev.weight < 0.5) return;
|
|
360
|
+
var section = sections.find(function (s) { return s.id === ev.sectionId; });
|
|
361
|
+
if (!section) return;
|
|
362
|
+
extractKeywords(section.text).forEach(function (kw) {
|
|
363
|
+
profile.topics[kw] = Math.min(1, (profile.topics[kw] || 0) * 0.95 + ev.weight * 0.05);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Dwell time → keyword reinforcement + style inference
|
|
368
|
+
Object.keys(dwellTotal).forEach(function (sid) {
|
|
369
|
+
var dwell = dwellTotal[sid];
|
|
370
|
+
if (dwell < 4) return;
|
|
371
|
+
var section = sections.find(function (s) { return s.id === sid; });
|
|
372
|
+
if (!section) return;
|
|
373
|
+
var dw = Math.min(1, dwell / 30) * 0.3;
|
|
374
|
+
|
|
375
|
+
extractKeywords(section.text).forEach(function (kw) {
|
|
376
|
+
profile.topics[kw] = Math.min(1, (profile.topics[kw] || 0) * 0.97 + dw * 0.03);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Style inference: long sections + high dwell → prefers detail
|
|
380
|
+
var wordCount = section.text.split(/\s+/).length;
|
|
381
|
+
if (wordCount > 120 && dwell >= 10) {
|
|
382
|
+
profile.style.prefersDetail = Math.min(1, profile.style.prefersDetail + 0.04);
|
|
383
|
+
} else if (wordCount > 120 && dwell < 2) {
|
|
384
|
+
// Skipped a long section → concise signal
|
|
385
|
+
profile.style.prefersDetail = Math.max(0, profile.style.prefersDetail - 0.03);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Technical preference: dwell on sections with technical vocabulary
|
|
389
|
+
var kws = extractKeywords(section.text);
|
|
390
|
+
var techMatches = kws.filter(function(k) { return TECHNICAL_WORDS.has(k); }).length;
|
|
391
|
+
if (techMatches >= 2 && dwell >= 6) {
|
|
392
|
+
profile.style.prefersTechnical = Math.min(1, profile.style.prefersTechnical + 0.04);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Prune to top 150 topics
|
|
397
|
+
var sorted = Object.entries(profile.topics).sort(function (a, b) { return b[1] - a[1]; });
|
|
398
|
+
profile.topics = Object.fromEntries(sorted.slice(0, 150));
|
|
399
|
+
profile.visits += 1;
|
|
400
|
+
profile.lastSeen = Date.now();
|
|
401
|
+
saveProfile(profile);
|
|
402
|
+
|
|
403
|
+
// Send anonymised data to server
|
|
404
|
+
// Include section preview text so the dashboard can show readable labels
|
|
405
|
+
var payload = JSON.stringify({
|
|
406
|
+
siteId: SITE_ID,
|
|
407
|
+
apiKey: API_KEY,
|
|
408
|
+
path: w.location.pathname,
|
|
409
|
+
scrollDepth: Math.round(scrollDepth * 100),
|
|
410
|
+
sid: getSessionId(),
|
|
411
|
+
abGroup: AB_GROUP,
|
|
412
|
+
sections: sections.map(function (s) {
|
|
413
|
+
return {
|
|
414
|
+
id: s.id,
|
|
415
|
+
tag: s.el.getAttribute('data-lens-tag'),
|
|
416
|
+
preview: s.preview, // first 60 chars — makes dashboard readable
|
|
417
|
+
dwell: Math.round(dwellTotal[s.id] || 0),
|
|
418
|
+
score: Math.round(s.score * 100),
|
|
419
|
+
words: s.text.split(/\s+/).length
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (navigator.sendBeacon) {
|
|
425
|
+
navigator.sendBeacon(API_URL + '/events', payload);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function getSessionId() {
|
|
430
|
+
var id = sessionStorage.getItem('_lens_sid');
|
|
431
|
+
if (!id) { id = Math.random().toString(36).substring(2, 11); sessionStorage.setItem('_lens_sid', id); }
|
|
432
|
+
return id;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Onboarding widget (publisher opt-in via data-onboard="true") ────────
|
|
436
|
+
function maybeShowOnboardWidget(profile) {
|
|
437
|
+
// Show once after 60s on first visit, only if no explicit preference set yet
|
|
438
|
+
if (!ONBOARD) return;
|
|
439
|
+
if (profile.style._explicit) return; // already answered
|
|
440
|
+
if (profile.visits > 1) return; // only first visit
|
|
441
|
+
|
|
442
|
+
setTimeout(function () {
|
|
443
|
+
ensureStyle(profile);
|
|
444
|
+
if (profile.style._explicit) return; // answered while we waited
|
|
445
|
+
|
|
446
|
+
var toast = d.createElement('div');
|
|
447
|
+
toast.id = '__lens_toast__';
|
|
448
|
+
toast.style.cssText = [
|
|
449
|
+
'position:fixed;bottom:24px;left:50%;transform:translateX(-50%)',
|
|
450
|
+
'background:#1e293b;color:#e2e8f0;border-radius:10px',
|
|
451
|
+
'padding:14px 18px;font-family:system-ui,sans-serif;font-size:13px',
|
|
452
|
+
'box-shadow:0 8px 28px rgba(0,0,0,.35);z-index:999999',
|
|
453
|
+
'display:flex;align-items:center;gap:12px;max-width:340px'
|
|
454
|
+
].join(';');
|
|
455
|
+
|
|
456
|
+
toast.innerHTML = [
|
|
457
|
+
'<span style="flex:1;line-height:1.4">Prefer the quick summary or full detail?</span>',
|
|
458
|
+
'<button id="__lens_concise__" style="background:#6366f1;color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Quick</button>',
|
|
459
|
+
'<button id="__lens_detail__" style="background:#334155;color:#e2e8f0;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Full detail</button>',
|
|
460
|
+
'<button id="__lens_dismiss__" style="background:none;border:none;color:#64748b;cursor:pointer;font-size:16px;line-height:1;padding:0 2px" title="Dismiss">×</button>'
|
|
461
|
+
].join('');
|
|
462
|
+
|
|
463
|
+
d.body.appendChild(toast);
|
|
464
|
+
|
|
465
|
+
function answer(val) {
|
|
466
|
+
var p = loadProfile();
|
|
467
|
+
ensureStyle(p);
|
|
468
|
+
p.style.prefersDetail = val;
|
|
469
|
+
p.style._explicit = true;
|
|
470
|
+
saveProfile(p);
|
|
471
|
+
toast.remove();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
d.getElementById('__lens_concise__').addEventListener('click', function () { answer(0.2); });
|
|
475
|
+
d.getElementById('__lens_detail__').addEventListener('click', function () { answer(0.8); });
|
|
476
|
+
d.getElementById('__lens_dismiss__').addEventListener('click', function () { toast.remove(); });
|
|
477
|
+
|
|
478
|
+
}, 60000);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Browser bridge helpers ──────────────────────────────────────────────
|
|
482
|
+
function fromBrowserProfile(p) {
|
|
483
|
+
var cp = p.contentPreferences || {};
|
|
484
|
+
return {
|
|
485
|
+
topics: p.topics || {},
|
|
486
|
+
visits: p.visits || 0,
|
|
487
|
+
lastSeen: Date.now(),
|
|
488
|
+
style: {
|
|
489
|
+
prefersDetail: cp.prefersDetail != null ? cp.prefersDetail : 0.5,
|
|
490
|
+
prefersTechnical: cp.prefersTechnical != null ? cp.prefersTechnical : 0.5,
|
|
491
|
+
prefersNarrative: cp.prefersNarrative != null ? cp.prefersNarrative : 0.5,
|
|
492
|
+
prefersBullets: cp.prefersBullets != null ? cp.prefersBullets : 0.5
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Rewrite mode ────────────────────────────────────────────────────────
|
|
498
|
+
function applyRewrittenSections(sections, adapted) {
|
|
499
|
+
var resultMap = {};
|
|
500
|
+
for (var i = 0; i < adapted.length; i++) {
|
|
501
|
+
resultMap[adapted[i].id] = adapted[i];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
sections.forEach(function (s) {
|
|
505
|
+
var result = resultMap[s.id];
|
|
506
|
+
if (!result) return;
|
|
507
|
+
|
|
508
|
+
var el = s.el;
|
|
509
|
+
|
|
510
|
+
// Update score so tracking and emphasis still work correctly
|
|
511
|
+
s.score = result.relevanceScore;
|
|
512
|
+
|
|
513
|
+
// Fade out → swap content → fade in
|
|
514
|
+
el.style.transition = 'opacity 150ms ease';
|
|
515
|
+
el.style.opacity = '0';
|
|
516
|
+
|
|
517
|
+
setTimeout(function () {
|
|
518
|
+
el.innerHTML = result.content;
|
|
519
|
+
el.setAttribute('data-lens-rewritten', 'true');
|
|
520
|
+
|
|
521
|
+
if (DEBUG) {
|
|
522
|
+
var badge = d.createElement('span');
|
|
523
|
+
badge.className = 'lens-rewrite-badge';
|
|
524
|
+
badge.textContent = 'AI ' + Math.round(result.relevanceScore * 100) + '%';
|
|
525
|
+
el.insertBefore(badge, el.firstChild);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
el.style.opacity = '1';
|
|
529
|
+
}, 160);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function runRewrite(profile, sections, expandEvents) {
|
|
534
|
+
// Need at least 3 topics; fall back to both mode if not
|
|
535
|
+
if (Object.keys(profile.topics).length < 3) {
|
|
536
|
+
if (DEBUG) console.log('[Lens] rewrite: fewer than 3 topics — falling back to both mode');
|
|
537
|
+
adapt(sections, profile, expandEvents);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Require API_URL to be set
|
|
542
|
+
if (!API_URL) {
|
|
543
|
+
if (DEBUG) console.log('[Lens] rewrite: no API_URL configured — falling back to both mode');
|
|
544
|
+
adapt(sections, profile, expandEvents);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
injectStyles();
|
|
549
|
+
|
|
550
|
+
var payload = JSON.stringify({
|
|
551
|
+
siteId: SITE_ID,
|
|
552
|
+
path: w.location.pathname,
|
|
553
|
+
sections: sections.map(function (s) { return { id: s.id, text: s.text }; }),
|
|
554
|
+
profile: {
|
|
555
|
+
topics: profile.topics,
|
|
556
|
+
contentPreferences: profile.style
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
fetch(API_URL + '/adapt', {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: Object.assign(
|
|
563
|
+
{ 'content-type': 'application/json' },
|
|
564
|
+
API_KEY ? { 'Authorization': 'Bearer ' + API_KEY } : {}
|
|
565
|
+
),
|
|
566
|
+
body: payload
|
|
567
|
+
})
|
|
568
|
+
.then(function (res) {
|
|
569
|
+
if (res.status === 503) {
|
|
570
|
+
if (DEBUG) console.log('[Lens] rewrite: server has no AI provider — falling back to both mode');
|
|
571
|
+
adapt(sections, profile, expandEvents);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
575
|
+
return res.json();
|
|
576
|
+
})
|
|
577
|
+
.then(function (data) {
|
|
578
|
+
if (!data) return; // already fell back
|
|
579
|
+
if (!data.sections || !data.sections.length) {
|
|
580
|
+
if (DEBUG) console.log('[Lens] rewrite: empty response — falling back to both mode');
|
|
581
|
+
adapt(sections, profile, expandEvents);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (DEBUG) console.log('[Lens] rewrite: applying ' + data.sections.length + ' rewritten sections');
|
|
585
|
+
applyRewrittenSections(sections, data.sections);
|
|
586
|
+
})
|
|
587
|
+
.catch(function (err) {
|
|
588
|
+
if (DEBUG) console.warn('[Lens] rewrite error (' + err.message + ') — falling back to both mode');
|
|
589
|
+
adapt(sections, profile, expandEvents);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function runInit(profile) {
|
|
594
|
+
ensureStyle(profile);
|
|
595
|
+
var sections = detectSections();
|
|
596
|
+
if (sections.length < 3) return;
|
|
597
|
+
|
|
598
|
+
sections.forEach(function (s) { s.score = scoreSection(s.text, profile); });
|
|
599
|
+
|
|
600
|
+
var expandEvents = []; // shared between adapt() and startTracking()
|
|
601
|
+
|
|
602
|
+
// A/B control group: track signals but skip all visual adaptation
|
|
603
|
+
if (AB_GROUP === 'control') {
|
|
604
|
+
if (DEBUG) console.log('[Lens] A/B group: control — tracking only, no adaptation');
|
|
605
|
+
startTracking(sections, expandEvents);
|
|
606
|
+
maybeShowOnboardWidget(profile);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (DEBUG && AB_GROUP === 'adapted') console.log('[Lens] A/B group: adapted');
|
|
611
|
+
|
|
612
|
+
if (MODE === 'rewrite') {
|
|
613
|
+
// Wait for DOMContentLoaded + 300ms to let page settle, then attempt rewrite
|
|
614
|
+
function doRewrite() {
|
|
615
|
+
setTimeout(function () {
|
|
616
|
+
runRewrite(profile, sections, expandEvents);
|
|
617
|
+
startTracking(sections, expandEvents);
|
|
618
|
+
maybeShowOnboardWidget(profile);
|
|
619
|
+
}, 300);
|
|
620
|
+
}
|
|
621
|
+
if (d.readyState === 'loading') {
|
|
622
|
+
d.addEventListener('DOMContentLoaded', doRewrite);
|
|
623
|
+
} else {
|
|
624
|
+
doRewrite();
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
adapt(sections, profile, expandEvents);
|
|
628
|
+
startTracking(sections, expandEvents);
|
|
629
|
+
maybeShowOnboardWidget(profile);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (DEBUG) {
|
|
633
|
+
console.log('[Lens] ' + sections.length + ' sections. Topics: ' +
|
|
634
|
+
Object.keys(profile.topics).length + '. Visits: ' + profile.visits + '. Mode: ' + MODE);
|
|
635
|
+
console.log('[Lens] Style — prefersDetail: ' + profile.style.prefersDetail.toFixed(2) +
|
|
636
|
+
', prefersTechnical: ' + profile.style.prefersTechnical.toFixed(2));
|
|
637
|
+
console.table(
|
|
638
|
+
sections.slice(0, 10).map(function(s) {
|
|
639
|
+
return { id: s.id, score: Math.round(s.score * 100) + '%', preview: s.preview };
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Init ────────────────────────────────────────────────────────────────
|
|
646
|
+
function init() {
|
|
647
|
+
// Inside Lens browser — use the cross-site profile instead of per-domain localStorage
|
|
648
|
+
if (w.__lens__ && w.__lens__.isBrowser) {
|
|
649
|
+
w.__lens__.getProfile()
|
|
650
|
+
.then(function (browserProfile) { runInit(fromBrowserProfile(browserProfile)); })
|
|
651
|
+
.catch(function () { runInit(loadProfile()); });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
runInit(loadProfile());
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (d.readyState === 'loading') { d.addEventListener('DOMContentLoaded', init); }
|
|
658
|
+
else { init(); }
|
|
659
|
+
|
|
660
|
+
}(window, document));
|