1/** 2 * dashboard-report-stage2.js â Dashboard å ±åé Stage 2 渲æ 3 * 4 * æå ¥é»ï¼`dashboard.html` å¨ dashboard-report-stage1.js ä¹å¾è¼å ¥æ¬æªã 5 * å軸ä¸è¦½å¡æå ¥æ¢æ `.score-grid`ï¼å« tabs äºåçåå§åå¡ï¼**ä¹å¾**ï¼ 6 * ä¸ç ´å£æ¢æ score-grid è demo.js çæ¸²æï¼ææææ¨çµ±è¨å±éæå¨ 7 * `[data-outcome-details]` ä¹åï¼å block ææäºï¼ã 8 * 9 * åå¡ï¼ 10 * - report-axes â åå¡ 4ï¼å軸å¥åº·åº¦ä¸è¦½ï¼ç¶²ä¸»èªè¨æäº + è¨æ¸å½ç¸½ï¼ 11 * - report-outcomes â åå¡ 5ï¼ææææ¨çµ±è¨å±éï¼A1/A2/B1 çµ±è¨å±¤ + proof levelï¼ 12 * 13 * åè¦æ¿çï¼ä¸å¯ç§»é¤ï¼è stage 1 ä¸è´ï¼ï¼ 14 * 1. pending_not_zeroï¼pending/partial ç¨ coral 強調ï¼share_of_voice çº null 15 * æé¡¯ç¤º share_of_voice_status åå + pending chipï¼çµä¸é¡¯ç¤º 0ã 16 * 2. 䏿°å¢ KPI 話è¡ï¼æ¯è¡ææ¡åªè® payload èªææ¬ä½ï¼limits å ¨æå¸¸é§ï¼ã 17 * 3. proof level 常é§ï¼æ¯åå±éå塿¨ç¤ºè³æçæé«èæçç´ã 18 */ 19(() => { 20 "use strict"; 21 22 /* Language follows document.body.dataset.lang at call time; demo.js rewrites 23 * that attribute from the visitor's preference, so a top-level snapshot would
24 * go stale. Copy resolves through the shared eight-locale catalog, keyed by 25 * the English string; the Chinese argument is the fallback when the catalog 26 * has no entry. A missing English argument means the string is Chinese-only. */ 27 const currentLang = () => document.body.dataset.lang || document.documentElement.lang || "en"; 28 const t = (c, e) => { 29 const catalog = window.XAgentI18nCatalog; 30 const source = e !== undefined ? e : c; 31 const key = catalog?.aliases?.[source] || source; 32 const translated = catalog?.messages?.[key]?.[currentLang()]; 33 if (translated) return translated; 34 return currentLang() === "zh-Hant" ? c : source; 35 }; 36 37 /* helpers ------------------------------------------------------------- */ 38 39 const el = (sel, root) => (root || document).querySelector(sel); 40 const pendingChip = (c, e) => `<span class="pending-chip">${t(c, e !== undefined ? e : c)}</span>`; 41 const escapeText = (v) => String(v ?? "").replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m])); 42 const read = (obj, ...path) => path.reduce((acc, k) => (acc && typeof acc === "object" ? acc[k] : undefined), obj); 43 44 const fmtPct = (v) => (typeof v === "number" ? `${v.toFixed(1)}%` : "â"); 45 46 /* åå¡ 4ï¼å軸å¥åº·åº¦ ------------------------------------------------------- 47 * æäºæ ¸å¿ï¼design brief §5.2ï¼ï¼ç¶²ä¸»æ¨é¡ + æè¡ç¸®å¯«ã大忏 + é²åº¦æ¢ + 48 * pass/partial/pending è¨æ¸ + ä¸å¥è©±çæ è§£è®ã */ 49
50 const AXIS_TITLES = { 51 seo: ["AI è®å¾æä½ ", "Read me"], 52 aeo: ["AI çå¾å°ä½ ", "Answer about me"], 53 geo: ["AI æ¢æ¨è¦ä½ ", "Recommend me"], 54 ago: ["AI è½ç´æ¥æ¥å®", "Take orders"], 55 }; 56 57 const axisVerdict = (dim, axisKey) => { 58 /* å ¨é¨ pass â 宿´çè§£ï¼æ partial â é»åºç¼ºå£ï¼å pending â å¾ éæ¸¬ */ 59 if (!dim) return [t("å¾ éæ¸¬", "Awaiting measurement"), "pending"]; 60 const { pass = 0, partial = 0, pending = 0 } = dim; 61 const total = pass + partial + pending; 62 if (total === 0) return [t("å¾ éæ¸¬", "Awaiting measurement"), "pending"]; 63 if (partial === 0 && pending === 0) { 64 const line = { 65 seo: [t("AI è½å®æ´çè§£ä½ çå ¬éé é¢", "AI can fully parse your public pages"), "pass"], 66 aeo: [t("AI è½å®æ´åçéæ¼ä½ çåé¡", "AI can fully answer questions about you"), "pass"], 67 geo: [t("AI æå®æ´ä¾æå¯æ¨è¦ä½ ", "AI has full evidence to recommend you"), "pass"], 68 ago: [t("AI è½å®æ´è®åä½ çä¸å®æµç¨", "AI can fully read your ordering flow"), "pass"], 69 }[axisKey] || ["", "pass"]; 70 return [line[0], line[1]]; 71 } 72 if (pending > 0) return [t("é¨åé ç®å¾ 鿏¬", "Some checks awaiting measurement"), "partial"]; 73 return [t("æå¯è£å¼·é ç®", "Improvable items found"), "partial"]; 74 }; 75 76 const renderAxes = (p) => { 77 const dims = read(p, "readiness", "dimensions") || {}; 78 const cards = Object.keys(AXIS_TITLES).map((key) => { 79 const dim = dims[key] || {}; 80 const [verdict, verdictState] = axisVerdict(dim, key); 81 const score = dim.score; 82 const fill = typeof score === "number" ? Math.max(0, Math.min(100, score)) : 0; 83 const hasScore = typeof score === "number"; 84 return ` 85 <article class="report-axis-card" data-report-axis="${key}"> 86 <header class="axis-head"> 87 <h3><span class="lang-zh">${t(...AXIS_TITLES[key])}</span></h3> 88 <p class="axis-abbr">${key.toUpperCase()}</p> 89 </header> 90 ${hasScore ? `<div class="axis-score-row"><span class="axis-score">${escapeText(score)}</span> 91 <div class="axis-bar" aria-hidden="true"><span class="axis-bar-fill" style="width:${fill}%"></span></div></div>` 92 : `<div class="axis-score-row"><span class="axis-score">â</span> 93 <div class="axis-bar" aria-hidden="true"><span class="axis-bar-fill" style="width:0"></span></div></div>`} 94 <p class="axis-counts">${t("éé", "pass")}: <b>${dim.pass ?? 0}</b> · ${t("é¨å", "partial")}: <b>${dim.partial ?? 0}</b> 95 ${dim.pending ? ` · ${t("å¾ éæ¸¬", "pending")}: <b class="axis-counts-pending">${escapeText(dim.pending)}</b>` : ""}</p> 96 <p class="axis-verdict ${verdictState}">${escapeText(verdict)}</p> 97 </article>`; 98 }); 99 return `<section class="report-axes" data-report-axes data-report-block="axes"> 100 <h2 class="report-section-title"><span class="lang-zh">ååééµåé¡</span><span class="lang-en">Four readiness axes</span></h2> 101 <div class="report-axes-grid">${cards.join("")}</div> 102 </section>`; 103 }; 104 105 /* åå¡ 5ï¼ææææ¨çµ±è¨å±é -------------------------------------------------- 106 * design brief §9.4ï¼éç´çºçµ±è¨å±éãA1 å±é by_provider/by_bot/by_status/ 107 * top_paths/last_seenï¼A2 å±é proof åç´ + by_engine/by_outcome/top_cited_hosts 108 * + mention/citation rate + share_of_voiceï¼B1 å±é by_event/by_source_class/ 109 * by_ai_sourceãpending_not_zeroï¼status çº pending çææ¨æ´å以 pending åç¾ï¼ 110 * å §é¨çµ±è¨ 0 å¼ä»¥ãå°æªè§æ¸¬ãåç¾èé 0 ç¾åæ¯æ·è¨ã */ 111 112 const OUTCOME_META = { 113 ai_crawler_reads: { 114 id: "a1", 115 title: ["A1 · AI ç¬è²è®å", "A1 · AI crawler reads"], 116 kind: "reads", 117 detailKey: "ai_reads", 118 }, 119 ai_citation_rate: { 120 id: "a2", 121 title: ["A2 · AI æåèå¼ç¨", "A2 · AI mentions & citations"], 122 kind: "citations", 123 detailKey: "ai_citations", 124 }, 125 ai_attributed_conversions: { 126 id: "b1", 127 title: ["B1 · AI æ¸å è½æ", "B1 · AI-attributed conversions"], 128 kind: "conversions", 129 detailKey: "conversions", 130 }, 131 }; 132 133 const objPairs = (obj) => { 134 if (!obj || typeof obj !== "object") return []; 135 return Object.entries(obj).filter(([, v]) => typeof v === "number" || typeof v === "string"); 136 }; 137 138 const renderStatDetail = (kind, d) => { 139 const rows = []; 140 const push = (labelZh, labelEn, value) => rows.push(`<tr><td>${t(labelZh, labelEn)}</td><td class="mono">${value}</td></tr>`); 141 142 if (kind === "reads") { 143 if (typeof d.total === "number") push("總è®å", "Total reads", escapeText(d.total)); 144 if (typeof d.successful === "number") push("æå", "Successful", escapeText(d.successful)); 145 if (typeof d.failed === "number") push("失æ", "Failed", escapeText(d.failed));
146 if (typeof d.bytes === "number") push("è®ååç¯", "Bytes fetched", escapeText(d.bytes)); 147 objPairs(d.by_provider).forEach(([k, v]) => push(`bot family · ${k}`, `bot family · ${k}`, escapeText(v))); 148 objPairs(d.by_bot).forEach(([k, v]) => push(`bot · ${k}`, `bot · ${k}`, escapeText(v))); 149 objPairs(d.by_status).forEach(([k, v]) => push(`status · ${k}`, `status · ${k}`, escapeText(v))); 150 if (d.last_seen) push("æå¾è§æ¸¬", "Last seen", escapeText(d.last_seen)); 151 } else if (kind === "citations") { 152 if (typeof d.total === "number") push("åå ±çæ¸", "Reported rows", escapeText(d.total)); 153 if (typeof d.reportable_runs === "number") push("å¯åå ±å·¡æª¢", "Reportable runs", escapeText(d.reportable_runs)); 154 if (typeof d.observed_with_artifact === "number") push("帶èæè§æ¸¬", "Observed with artifact", escapeText(d.observed_with_artifact)); 155 if (typeof d.observed_manual_import === "number") push("æåå¯å ¥", "Manual import", escapeText(d.observed_manual_import)); 156 if (typeof d.manual_pending === "number") push("æåå¾ èç", "Manual pending", escapeText(d.manual_pending)); 157 if (typeof d.synthetic_not_reportable === "number") push("模æ¬ä¸å¯åå ±", "Synthetic (not reportable)", escapeText(d.synthetic_not_reportable)); 158 if (typeof d.mention_rate_pct === "number") push("æåç", "Mention rate", fmtPct(d.mention_rate_pct)); 159 if (typeof d.citation_rate_pct === "number") push("å¼ç¨ç", "Citation rate", fmtPct(d.citation_rate_pct)); 160 if (typeof d.share_of_voice_pct === "number" && !d.share_of_voice_status) { 161 push("è²éå æ¯", "Share of voice", fmtPct(d.share_of_voice_pct)); 162 } else { 163 const reason = d.share_of_voice_status || t("éå ä¾å ¥ç«¶åæåï¼æè½è¨ç®è²éå æ¯", "Competitor mentions required before voice share can be computed"); 164 push("è²éå æ¯", "Share of voice", `${pendingChip(t("å¾ åå¾", "pending"))} ${escapeText(reason)}`); 165 } 166 objPairs(d.by_engine).forEach(([k, v]) => push(`engine · ${k}`, `engine · ${k}`, escapeText(v))); 167 objPairs(d.by_outcome).forEach(([k, v]) => push(`outcome · ${k}`, `outcome · ${k}`, escapeText(v))); 168 (d.top_cited_hosts || []).forEach((h) => push("å¼ç¨ä¾æº", "Cited host", escapeText(h))); 169 if (d.last_seen) push("æå¾è§æ¸¬", "Last seen", escapeText(d.last_seen)); 170 } else if (kind === "conversions") { 171 if (typeof d.total === "number") push("總äºä»¶", "Total events", escapeText(d.total)); 172 if (typeof d.ai_attributed === "number") push("AI æ¸å ", "AI-attributed", escapeText(d.ai_attributed)); 173 if (typeof d.non_ai_attributed === "number") push("é AI æ¸å ", "Non-AI-attributed", escapeText(d.non_ai_attributed)); 174 if (typeof d.total_value === "number") push("總éé¡", "Total value", escapeText(d.total_value)); 175 if (typeof d.ai_attributed_value === "number") push("AI æ¸å éé¡", "AI-attributed value", escapeText(d.ai_attributed_value)); 176 if (typeof d.ai_attributed_rate_pct === "number") push("AI æ¸å æ¯ç", "AI-attributed rate", fmtPct(d.ai_attributed_rate_pct)); 177 objPairs(d.by_event).forEach(([k, v]) => push(`event · ${k}`, `event · ${k}`, escapeText(v))); 178 objPairs(d.by_source_class).forEach(([k, v]) => push(`source class · ${k}`, `source class · ${k}`, escapeText(v))); 179 objPairs(d.by_ai_source).forEach(([k, v]) => push(`AI source · ${k}`, `AI source · ${k}`, escapeText(v))); 180 if (d.last_seen) push("æå¾è§æ¸¬", "Last seen", escapeText(d.last_seen)); 181 } 182 if (rows.length === 0) return `<p class="outcome-no-data">${t("å°æªè§æ¸¬å°çµ±è¨è³æ", "No statistics observed yet")}</p>`; 183 return `<table class="outcome-detail-table">${rows.join("")}</table>`; 184 }; 185 186 const renderOutcomes = (p) => { 187 const outcomes = read(p, "outcomes") || {}; 188 const details = read(p, "outcome_details") || {}; 189 const metaKeys = Object.keys(OUTCOME_META); 190 191 const rows = metaKeys.map((key) => { 192 const o = outcomes[key] || {}; 193 const d = details[OUTCOME_META[key].detailKey] || {}; 194 const meta = OUTCOME_META[key]; 195 const statusPending = o.status === "pending"; 196 const proofLevel = o.proof_level || read(d, "highest_proof_level") || "not_observed"; 197 const limits = d.limits || []; 198 199 const valueCell = o.value != null ? escapeText(o.value) 200 : statusPending ? `${pendingChip(t("å¾ éæ¸¬", "pending"))}
200 ${t("鿢æ CDNï¼ä¼ºæå¨ï¼CRM è³æ", "Requires existing CDN / server / CRM data")}` 201 : "â"; 202 203 return ` 204 <div class="outcome-row ${statusPending ? "outcome-pending" : ""}" data-report-outcome="${meta.id}"> 205 <div class="outcome-head"> 206 <div> 207 <h3 class="outcome-title">${t(...meta.title)}</h3> 208 <p class="outcome-proof">${t("èæçç´", "Proof level")}ï¼<b>${escapeText(proofLevel)}</b> 209 ${o.source ? ` · <a href="${escapeText(o.source)}" target="_blank" rel="noopener">${escapeText(o.source)}</a>` : ""}</p> 210 </div> 211 <div class="outcome-value-wrap"><span class="outcome-value">${valueCell}</span></div> 212 </div> 213 <p class="outcome-detail">${escapeText(read(o, "detail") || "")}</p> 214 <details class="outcome-expand"> 215 <summary><span class="lang-zh">å±éçµ±è¨æç´°</span><span class="lang-en">Expand statistics</span></summary> 216 ${renderStatDetail(meta.kind, d)} 217 ${limits.length ? `<p class="outcome-limits">${limits.map((l) => pendingChip(l)).join(" ")}</p>` : ""} 218 </details> 219 </div>`; 220 }); 221 222 return `<section class="report-outcomes" data-report-outcomes data-report-block="outcomes"> 223 <h2 class="report-section-title"><span class="lang-zh">ææææ¨ï¼ä½ å¯ä»¥èªå·±æ¥ï¼</span><span class="lang-en">Outcome metrics â verifiable by you</span></h2> 224 <div class="report-outcomes-list">${rows.join("")}</div> 225 </section>`; 226 }; 227 228 /* æè¼ ------------------------------------------------------------------ */ 229 230 const mountSections = (p) => { 231 const root = el("[data-dashboard-content]"); 232 if (!root) return false; 233 234 const axesEl = document.createElement("div"); 235 axesEl.innerHTML = renderAxes(p); 236 const scoreGrid = el(".score-grid", root); 237 if (scoreGrid) scoreGrid.after(axesEl.firstElementChild); 238 else root.append(axesEl.firstElementChild); 239 240 const outcomesEl = document.createElement("div"); 241 outcomesEl.innerHTML = renderOutcomes(p); 242 const outcomeDetails = el("[data-outcome-details]", root); 243 if (outcomeDetails) outcomeDetails.before(outcomesEl.firstElementChild); 244 else { 245 const priorities = el("[data-priorities]", root) || el("[data-receipt]", root); 246 if (priorities) priorities.before(outcomesEl.firstElementChild); 247 else root.append(outcomesEl.firstElementChild); 248 } 249 return true; 250 }; 251 252 /* åå§åï¼è stage1 åä¸è§å¯å¨è§¸ç¼ï¼è¦ stage1 ç MutationObserverï¼ï¼ 253 * æ¬æªåªè¨»åæ ¹ç©ä»¶ï¼é¿å éè¤è§å¯ã */ 254 const init = () => { 255 const root = el("[data-dashboard-content]"); 256 if (!root) return; 257 root.querySelectorAll("[data-report-block='axes'], [data-report-outcomes]").forEach((node) => node.remove()); 258 const context = window.AgentIdReportApi?.current(); 259 if (context) mountSections(context.data); 260 }; 261 262 window.AgentIdReportStage2 = Object.freeze({ init, renderAxes, renderOutcomes }); 263 264 /* é é¢å·²è§£éçç´æ¥è§¸ç¼ */ 265 window.addEventListener("dashboard-report-change", init); 266 init(); 267})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.