Pavle-17 commited on
Commit
7f2d10b
·
verified ·
1 Parent(s): 04f00b1

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +1 -0
  2. infoshield-demo.js +355 -0
index.html CHANGED
@@ -21,5 +21,6 @@
21
  </head>
22
  <body>
23
  <div id="root"></div>
 
24
  </body>
25
  </html>
 
21
  </head>
22
  <body>
23
  <div id="root"></div>
24
+ <script defer src="/infoshield-demo.js"></script>
25
  </body>
26
  </html>
infoshield-demo.js ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* InfoShield — baked-in demo overlays for the public (extension-less) site.
2
+ *
3
+ * On the hosted static deployment there is no Chrome extension, so this script
4
+ * reproduces exactly what the extension does: it reads the cached analysis from
5
+ * feed.json, injects the same `isx-` overlays onto each [data-post-id], and
6
+ * posts the same `scan` message that makes the InfoShield Insights widget
7
+ * appear. It only runs on a non-localhost host, so local dev + the real
8
+ * extension are untouched. Ported from extension/content.js (verbatim styles).
9
+ */
10
+ (() => {
11
+ "use strict";
12
+ const host = location.hostname.toLowerCase();
13
+ if (["localhost", "127.0.0.1", "0.0.0.0", "::1", ""].includes(host)) return; // extension handles local dev
14
+
15
+ const BADGE = "/badge.png";
16
+ const TECH_LABELS = {
17
+ fear_amplification: "Fear Amplification", scapegoating: "Scapegoating",
18
+ us_vs_them: "Us vs. Them", dehumanizing_language: "Dehumanizing Language",
19
+ false_urgency: "False Urgency", emotional_manipulation: "Emotional Manipulation",
20
+ identity_targeting: "Identity Targeting", ad_hominem: "Ad Hominem",
21
+ };
22
+ const TECH_CHIP = {
23
+ false_urgency: "isx-chip--red", fear_amplification: "isx-chip--amber",
24
+ us_vs_them: "isx-chip--teal", scapegoating: "isx-chip--amber",
25
+ dehumanizing_language: "isx-chip--rose", emotional_manipulation: "isx-chip--violet",
26
+ identity_targeting: "isx-chip--rose", ad_hominem: "isx-chip--slate",
27
+ };
28
+ const STATE = { analyses: new Map(), flagged: new Set(), ready: false };
29
+
30
+ const el = (tag, cls, text) => {
31
+ const n = document.createElement(tag);
32
+ if (cls) n.className = cls;
33
+ if (text != null) n.textContent = text;
34
+ return n;
35
+ };
36
+ const confidenceLabel = (c) => {
37
+ const pct = Math.round((c || 0) * 100);
38
+ return c >= 0.8 ? `Detected with ${pct}% confidence` : `May contain indicators (${pct}% confidence)`;
39
+ };
40
+ const sourceLabel = (url) => {
41
+ try {
42
+ const h = new URL(url).hostname.replace(/^www\./, "");
43
+ if (h.includes("ohchr")) return "OHCHR";
44
+ if (h.includes("unesco")) return "UNESCO";
45
+ if (h.includes("treaties.un")) return "UN Treaty";
46
+ if (h.includes("un.org") || h.includes("digitallibrary.un")) return "UN";
47
+ return h;
48
+ } catch { return "source"; }
49
+ };
50
+
51
+ function highlightIn(node, phrases, on) {
52
+ node.querySelectorAll("mark.isx-highlight").forEach((m) => {
53
+ m.replaceWith(document.createTextNode(m.textContent));
54
+ });
55
+ node.normalize();
56
+ if (!on) return;
57
+ for (const phrase of phrases) {
58
+ const needle = (phrase || "").toLowerCase();
59
+ if (!needle) continue;
60
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
61
+ acceptNode: (tn) =>
62
+ tn.parentElement && tn.parentElement.closest(".isx-wrap")
63
+ ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT,
64
+ });
65
+ let tn, done = false;
66
+ while (!done && (tn = walker.nextNode())) {
67
+ const idx = tn.nodeValue.toLowerCase().indexOf(needle);
68
+ if (idx === -1) continue;
69
+ const after = tn.splitText(idx);
70
+ after.splitText(needle.length);
71
+ const mark = el("mark", "isx-highlight");
72
+ mark.textContent = after.nodeValue;
73
+ after.replaceWith(mark);
74
+ done = true;
75
+ }
76
+ }
77
+ }
78
+
79
+ function buildShield() {
80
+ const wrap = el("span", "isx-shield");
81
+ for (let i = 0; i < 3; i++) wrap.appendChild(el("span", "isx-shield__ring"));
82
+ const img = el("img", "isx-shield__core");
83
+ img.src = BADGE; img.alt = "InfoShield";
84
+ wrap.appendChild(img);
85
+ return wrap;
86
+ }
87
+
88
+ function buildAnalysis(node, entry) {
89
+ const { classification, analysis } = entry;
90
+ const wrap = el("div", "isx-wrap");
91
+ let expanded = false;
92
+ const phrases = (analysis.highlights || []).map((h) => h.phrase);
93
+
94
+ const bar = el("button", "isx-bar");
95
+ bar.appendChild(buildShield());
96
+ bar.appendChild(el("span", "isx-bar__conf", confidenceLabel(classification.confidence)));
97
+ const moreText = el("span", "isx-bar__more", "What's this?");
98
+ bar.appendChild(moreText);
99
+ wrap.appendChild(bar);
100
+
101
+ const card = el("div", "isx-card");
102
+ card.style.display = "none";
103
+ card.appendChild(el("div", "isx-card__title", "Why was this flagged?"));
104
+ const chips = el("div", "isx-chips");
105
+ (analysis.techniques || []).forEach((t) =>
106
+ chips.appendChild(el("span", `isx-chip ${TECH_CHIP[t] || "isx-chip--violet"}`, TECH_LABELS[t] || t))
107
+ );
108
+ card.appendChild(chips);
109
+ if (analysis.explanation) card.appendChild(el("p", "isx-expl", analysis.explanation));
110
+ if (analysis.recommendation) card.appendChild(el("div", "isx-rec", analysis.recommendation));
111
+ if (analysis.bias_check) {
112
+ const bc = el("div", "isx-biascheck");
113
+ bc.appendChild(el("b", null, "Rights check: "));
114
+ bc.appendChild(document.createTextNode(analysis.bias_check));
115
+ card.appendChild(bc);
116
+ }
117
+ if (analysis.citations && analysis.citations.length) {
118
+ const src = el("div", "isx-sources");
119
+ src.appendChild(el("b", null, "Sources:"));
120
+ analysis.citations.forEach((url) => {
121
+ const a = el("a", "isx-source-link", sourceLabel(url));
122
+ a.href = url; a.target = "_blank"; a.rel = "noopener noreferrer";
123
+ src.appendChild(a);
124
+ });
125
+ card.appendChild(src);
126
+ }
127
+ wrap.appendChild(card);
128
+
129
+ bar.addEventListener("click", () => {
130
+ expanded = !expanded;
131
+ card.style.display = expanded ? "block" : "none";
132
+ moreText.textContent = expanded ? "Hide" : "What's this?";
133
+ highlightIn(node, phrases, expanded);
134
+ });
135
+ return wrap;
136
+ }
137
+
138
+ function injectNode(node) {
139
+ if (node.dataset.isxInjected === "1") return;
140
+ const entry = STATE.analyses.get(node.getAttribute("data-post-id"));
141
+ node.dataset.isxInjected = "1";
142
+ if (!entry || !entry.flagged || !entry.analysis) return;
143
+ node.appendChild(buildAnalysis(node, entry));
144
+ STATE.flagged.add(node.getAttribute("data-post-id"));
145
+ }
146
+ function injectAll() {
147
+ document.querySelectorAll("[data-post-id]").forEach(injectNode);
148
+ }
149
+
150
+ function computeScan() {
151
+ const scanned = document.querySelectorAll("[data-post-id]").length;
152
+ const counter = {};
153
+ STATE.flagged.forEach((id) => {
154
+ (STATE.analyses.get(id)?.analysis?.techniques || []).forEach((t) => (counter[t] = (counter[t] || 0) + 1));
155
+ });
156
+ let top = null, max = 0;
157
+ for (const [t, c] of Object.entries(counter)) if (c > max) { max = c; top = t; }
158
+ return {
159
+ scanned, flagged: STATE.flagged.size,
160
+ patternsDetected: Object.keys(counter).length,
161
+ mostCommonPattern: top ? (TECH_LABELS[top] || top) : "—",
162
+ };
163
+ }
164
+ function postScan() {
165
+ if (!STATE.ready) return;
166
+ window.postMessage({ source: "infoshield", type: "scan", payload: computeScan() }, "*");
167
+ }
168
+
169
+ function injectStyles() {
170
+ if (document.getElementById("isx-demo-style")) return;
171
+ const s = el("style"); s.id = "isx-demo-style"; s.textContent = ISX_CSS;
172
+ document.head.appendChild(s);
173
+ }
174
+
175
+ async function start() {
176
+ injectStyles();
177
+ try {
178
+ const base = (document.querySelector("base")?.getAttribute("href") || "/").replace(/\/$/, "");
179
+ const res = await fetch(`${base}/feed.json`, { cache: "no-store" });
180
+ const feed = await res.json();
181
+ (feed.posts || []).forEach((p) =>
182
+ STATE.analyses.set(p.id, { classification: p.classification, analysis: p.analysis, flagged: p.flagged })
183
+ );
184
+ } catch (e) { return; }
185
+
186
+ STATE.ready = true;
187
+ injectAll();
188
+ postScan();
189
+
190
+ new MutationObserver((muts) => {
191
+ let added = false;
192
+ for (const m of muts) for (const n of m.addedNodes) {
193
+ if (n.nodeType !== 1) continue;
194
+ if (n.matches?.("[data-post-id]")) { injectNode(n); added = true; }
195
+ n.querySelectorAll?.("[data-post-id]").forEach((node) => { injectNode(node); added = true; });
196
+ }
197
+ if (added) postScan();
198
+ }).observe(document.body, { childList: true, subtree: true });
199
+ }
200
+
201
+ // The React page asks for the latest scan once its listener mounts.
202
+ window.addEventListener("message", (e) => {
203
+ const m = e.data;
204
+ if (m && m.source === "infoshield-page" && m.type === "request-scan") postScan();
205
+ });
206
+
207
+ const ISX_CSS = `/* InfoShield injected overlay styles. All classes are prefixed \`isx-\` so they
208
+ cannot collide with the host page's CSS. */
209
+
210
+ .isx-wrap {
211
+ margin: 12px 0 2px 56px;
212
+ font-family: Inter, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
213
+ }
214
+
215
+ /* --- animated shield (sits at the left of the status bar) -----------------
216
+ Exact port of the former top-right post badge: identical core size,
217
+ box-shadow, rings, ping timing/scale/opacity and reduced-motion handling.
218
+ Only the positioning differs (inline in the bar, not absolute). */
219
+ .isx-shield {
220
+ position: relative;
221
+ flex: none;
222
+ width: 30px;
223
+ height: 30px;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ }
228
+ .isx-shield__core {
229
+ position: relative;
230
+ z-index: 2;
231
+ width: 30px;
232
+ height: 30px;
233
+ border-radius: 9999px;
234
+ box-shadow: 0 2px 8px rgba(76, 29, 149, 0.35);
235
+ }
236
+ .isx-shield__ring {
237
+ position: absolute;
238
+ inset: 0;
239
+ z-index: 1;
240
+ border-radius: 9999px;
241
+ background: radial-gradient(
242
+ circle,
243
+ rgba(124, 58, 237, 0.35) 0%,
244
+ rgba(124, 58, 237, 0.18) 60%,
245
+ rgba(124, 58, 237, 0) 70%
246
+ );
247
+ animation: isx-ping 2.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
248
+ }
249
+ .isx-shield__ring:nth-child(2) { animation-delay: 0.9s; }
250
+ .isx-shield__ring:nth-child(3) { animation-delay: 1.8s; }
251
+
252
+ @keyframes isx-ping {
253
+ 0% { transform: scale(1); opacity: 0.55; }
254
+ 70% { opacity: 0.12; }
255
+ 100% { transform: scale(2.6); opacity: 0; }
256
+ }
257
+ @media (prefers-reduced-motion: reduce) {
258
+ .isx-shield__ring { animation: none; transform: scale(1.6); opacity: 0.18; }
259
+ .isx-shield__ring:nth-child(3) { display: none; }
260
+ }
261
+
262
+ /* --- collapsed status bar ------------------------------------------------- */
263
+ .isx-bar {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 11px;
267
+ width: 100%;
268
+ text-align: left;
269
+ border: 1px solid #e9e3ff;
270
+ background: #f4f1fe;
271
+ border-radius: 14px;
272
+ padding: 9px 14px;
273
+ cursor: pointer;
274
+ font-size: 13.5px;
275
+ color: #0f1419;
276
+ transition: background 0.15s ease;
277
+ }
278
+ .isx-bar:hover { background: #efeafe; }
279
+ .isx-bar__conf { color: rgba(15, 20, 25, 0.62); font-weight: 500; }
280
+ .isx-bar__more { margin-left: auto; color: #7c3aed; font-size: 13px; font-weight: 600; }
281
+
282
+ /* --- expanded card -------------------------------------------------------- */
283
+ .isx-card {
284
+ margin-top: 8px;
285
+ border: 1px solid #e9e3ff;
286
+ background: #f7f5ff;
287
+ border-radius: 16px;
288
+ padding: 16px;
289
+ }
290
+ .isx-card__title {
291
+ font-size: 13px;
292
+ font-weight: 700;
293
+ color: #6d28d9;
294
+ text-decoration: underline;
295
+ text-underline-offset: 2px;
296
+ margin-bottom: 12px;
297
+ }
298
+
299
+ .isx-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
300
+ .isx-chip {
301
+ border-radius: 9999px;
302
+ background: #fff;
303
+ border: 1px solid #e5e7eb;
304
+ color: #374151;
305
+ font-size: 12.5px;
306
+ font-weight: 600;
307
+ padding: 4px 12px;
308
+ }
309
+ .isx-chip--red { color: #e11d48; border-color: #fecdd3; background: #fff1f2; }
310
+ .isx-chip--amber { color: #d97706; border-color: #fde68a; background: #fffbeb; }
311
+ .isx-chip--teal { color: #0d9488; border-color: #99f6e4; background: #f0fdfa; }
312
+ .isx-chip--violet { color: #7c3aed; border-color: #ddd6fe; background: #f5f3ff; }
313
+ .isx-chip--rose { color: #be185d; border-color: #fbcfe8; background: #fdf2f8; }
314
+ .isx-chip--slate { color: #475569; border-color: #e2e8f0; background: #f8fafc; }
315
+
316
+ .isx-expl { font-size: 14px; line-height: 1.6; color: rgba(15, 20, 25, 0.78); margin: 0; }
317
+ .isx-rec { margin-top: 10px; font-size: 13px; color: rgba(15, 20, 25, 0.7); line-height: 1.5; }
318
+ .isx-biascheck { margin-top: 10px; font-size: 12px; color: #6b7280; line-height: 1.5; }
319
+ .isx-biascheck b { color: rgba(15, 20, 25, 0.7); }
320
+
321
+ .isx-sources {
322
+ margin-top: 12px;
323
+ font-size: 12px;
324
+ color: #6b7280;
325
+ display: flex;
326
+ align-items: center;
327
+ flex-wrap: wrap;
328
+ gap: 6px;
329
+ }
330
+ .isx-sources b { color: rgba(15, 20, 25, 0.7); }
331
+ .isx-source-link {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 5px;
335
+ color: #6d28d9;
336
+ text-decoration: none;
337
+ background: #fff;
338
+ border: 1px solid #e9e3ff;
339
+ border-radius: 9999px;
340
+ padding: 2px 10px;
341
+ font-weight: 600;
342
+ }
343
+ .isx-source-link:hover { background: #f5f3ff; }
344
+
345
+ .isx-highlight {
346
+ background: linear-gradient(180deg, #ede9fe 0%, #ddd6fe 100%);
347
+ border-bottom: 2px solid #a78bfa;
348
+ border-radius: 3px;
349
+ padding: 0 2px;
350
+ }
351
+ `;
352
+
353
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
354
+ else start();
355
+ })();