Files
ai-kefu-admin/static/widget-test.html
T

213 lines
10 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Widget Test — Invite Card + Theme Color + Assertions</title>
<!-- 断言面板依赖 window.ChatTheme(与 widget.js 内联拷贝做一致性比对) -->
<script src="/static/js/theme.js"></script>
<style>
body {
font-family: Arial, sans-serif;
padding: 40px;
background: #f5f5f5;
}
.info {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 { color: #333; margin-bottom: 10px; }
h2 { color: #333; margin-bottom: 10px; }
p { color: #666; line-height: 1.6; }
ul { color: #666; }
.test-list li { margin-bottom: 8px; }
.test-list li::before { content: "☐ "; color: #066fc0; font-weight: bold; }
#assertions div { padding: 4px 0; border-bottom: 1px solid #eee; font-family: Consolas, monospace; font-size: 13px; }
#assertions div:last-child { border-bottom: none; }
#colorPicker { width: 48px; height: 32px; padding: 0; border: 1px solid #ccc; border-radius: 4px; vertical-align: middle; cursor: pointer; }
.btn { padding: 8px 16px; background: #066fc0; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 6px; }
.btn:hover { opacity: 0.9; }
</style>
</head>
<body>
<div class="info">
<h1>Widget Invite Card Test Page</h1>
<p>This page tests the auto-popup invite card feature.</p>
<p><strong>Expected behavior:</strong></p>
<ul class="test-list">
<li>Invite card appears after 5 seconds</li>
<li>Invite card has brand-color gradient header with robot emoji</li>
<li>Topic buttons come from the tenant brand config (<code>/api/public/brand</code> → topics)</li>
<li>Action buttons: "Start Chat" and "No Thanks"</li>
<li>Click a topic button → invite closes → chat opens → message auto-sends</li>
<li>Click "Start Chat" → invite closes → chat opens</li>
<li>Click "No Thanks" → invite permanently closes</li>
<li>Click "No Thanks" or X → refresh → invite stays hidden AND the old card area stays clickable (no invisible overlay; regression 2026-09-01)</li>
<li>Click toggle button (while invite visible) → invite closes → chat opens</li>
<li>Refresh page → invite appears again after 5s (unless "No Thanks" was clicked)</li>
</ul>
</div>
<div class="info">
<h2>Popular Topics in Chat Panel</h2>
<ul class="test-list">
<li>Chat panel shows "POPULAR TOPICS" section with the tenant's topic buttons</li>
<li>Click topic → message auto-sends → topics section hides</li>
</ul>
</div>
<div class="info">
<h2>Theme Color Preview</h2>
<p>Pick a color and Apply, or use a preset. Each click tears down the old widget (host + script + invite dismissal state) and re-embeds with <code>data-color</code>. "Test Broken Avatar" points <code>data-avatar</code> at a missing image to verify the 🤖 emoji fallback.</p>
<p>
<input type="color" id="colorPicker" value="#066fc0">
<button class="btn" onclick="applyColor()">Apply</button>
<button class="btn" onclick="applyColor('#066fc0')">Brand Blue</button>
<button class="btn" onclick="applyColor('#1a73e8')">Blue</button>
<button class="btn" onclick="applyColor('#16a34a')">Green</button>
<button class="btn" onclick="applyColor('#7c3aed')">Purple</button>
<button class="btn" onclick="rebuildWidget({'data-mobile':'false','data-color':document.getElementById('colorPicker').value})">Desktop Pill</button>
<button class="btn" onclick="applyBrokenAvatar()">Test Broken Avatar</button>
</p>
</div>
<div class="info">
<h2>Assertion Panel</h2>
<p>Runs on every widget (re)embed. Checks: derived default palette vs current hardcoded values (max per-channel Δ ≤ 8), derive(default) === DEFAULT_PALETTE, inline widget.js copy === theme.js copy, invalid input falls back to DEFAULT_PALETTE.</p>
<div id="assertions"></div>
</div>
<div class="info">
<p><strong>Current Page:</strong> <span id="pathname"></span></p>
<p><strong>Storage Key:</strong> widget_invite_dismissed_&lt;slug&gt;_<span id="storage-key"></span> (Task 10: key 按租户 slug 命名空间化)</p>
<p><button class="btn" onclick="clearStorage()">Clear Storage &amp; Refresh</button></p>
</div>
<script>
document.getElementById('pathname').textContent = window.location.pathname;
document.getElementById('storage-key').textContent = window.location.pathname;
function clearStorage() {
// Task 10: key 已租户化(widget_invite_dismissed_<slug>_<path>)——按前缀清扫全部
try {
Object.keys(sessionStorage).forEach(function (k) {
if (k.indexOf('widget_invite_dismissed_') === 0) sessionStorage.removeItem(k);
});
} catch (e) {}
location.reload();
}
/* ===== Teardown / 重建(spec §7.1 三个陷阱:旧 host、旧 script、sessionStorage) ===== */
var BASE_ATTRS = {
"data-position": "bottom-right",
"data-theme": "light",
"data-mobile": "true",
"data-debug": "true",
"data-whatsapp": "https://wa.me/8613800138000",
"data-email": "mailto:demo@example.com"
};
var rebuildLoading = false, rebuildPending = null;
function removeWidgetHosts() {
// Task 10: host id 已租户化(cc-widget-host-<slug>),按前缀清扫
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
for (var i = 0; i < hosts.length; i++) hosts[i].remove();
// 复审: 清掉旧挂件的 debug 钩子——waitForWidget 轮询它,旧钩子残留
// 会让重建中的断言误读"新挂件已挂载"
try { delete window.__chatWidgetDebug; } catch (e) {}
}
function rebuildWidget(extra) {
if (rebuildLoading) { rebuildPending = extra || {}; return; } // 快速连点:只记最新意图,当前加载完接力重建
rebuildLoading = true;
document.getElementById("assertions").innerHTML = "<div>…rebuilding…</div>";
removeWidgetHosts();
document.querySelectorAll("script[src*='widget.js']").forEach(function (s) { s.remove(); });
try {
Object.keys(sessionStorage).forEach(function (k) {
if (k.indexOf("widget_invite_dismissed_") === 0) sessionStorage.removeItem(k);
});
} catch (e) {}
var s = document.createElement("script");
s.src = "/static/widget.js?" + Date.now();
var attrs = Object.assign({}, BASE_ATTRS, extra || {});
Object.keys(attrs).forEach(function (k) { s.setAttribute(k, attrs[k]); });
s.onload = function () {
rebuildLoading = false;
// Task 10: 挂件改等 brand 端点落定后才挂载——脚本 onload 早于挂载,
// 轮询 __chatWidgetDebug(boot 里同步设置,观察到场即 init 已完成)
// 就绪再清竞态残留并跑断言
waitForWidget(function () {
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
for (var i = 0; i < hosts.length - 1; i++) hosts[i].remove(); // 竞态兜底:只保留最后挂载
runAssertions(); // 挂件未就绪(超时)时其内部 !d 分支自行报红
});
var next = rebuildPending; rebuildPending = null;
if (next) rebuildWidget(next);
};
s.onerror = function () {
rebuildLoading = false;
document.getElementById("assertions").innerHTML = "<div>❌ widget.js 加载失败</div>";
var next = rebuildPending; rebuildPending = null;
if (next) rebuildWidget(next);
};
document.body.appendChild(s);
}
function waitForWidget(cb) {
var started = Date.now();
(function poll() {
if (window.__chatWidgetDebug) return cb(true);
if (Date.now() - started > 5000) return cb(false); // brand 挂起/失败兜底
setTimeout(poll, 100);
})();
}
function applyColor(c) {
rebuildWidget({ "data-color": c || document.getElementById("colorPicker").value });
}
function applyBrokenAvatar() {
rebuildWidget({ "data-color": "#1a73e8", "data-avatar": "/static/nope.png" });
}
/* ===== 断言面板(spec §8.2) ===== */
var CURRENT_HARDCODED = { // 现状代表值(HESUPIPE 品牌蓝 #066fc0 派生),Δ ≤ 8/通道
primary: "#066fc0", gradientEnd: "#0666bb", deepText: "#14517a",
captionText: "#2174ab", softBg: "#f3f9fe", borderSoft: "#bbdcf2"
};
function hexToRgb6(h) {
h = String(h).replace(/^#/, "");
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}
function channelDelta(a, b) {
var ra = hexToRgb6(a), rb = hexToRgb6(b);
return Math.max(Math.abs(ra[0] - rb[0]), Math.abs(ra[1] - rb[1]), Math.abs(ra[2] - rb[2]));
}
function runAssertions() {
var rows = [];
function check(name, ok, detail) { rows.push((ok ? "✅ " : "❌ ") + name + (detail ? " — " + detail : "")); }
var t = window.ChatTheme, d = window.__chatWidgetDebug;
if (!t || !d) {
rows.push("❌ theme.js / widget debug 钩子未就绪");
} else {
var p = t.derivePalette("#066fc0");
Object.keys(CURRENT_HARDCODED).forEach(function (k) {
var delta = channelDelta(p[k], CURRENT_HARDCODED[k]);
check("default " + k + " Δ≤8", delta <= 8, p[k] + " vs " + CURRENT_HARDCODED[k] + ", Δ=" + delta);
});
check("derive(default) === DEFAULT_PALETTE",
JSON.stringify(p) === JSON.stringify(t.DEFAULT_PALETTE));
check("inline copy === theme.js copy",
d.derivePalette.toString().replace(/\s+/g, "") === t.derivePalette.toString().replace(/\s+/g, ""));
check("invalid color → DEFAULT_PALETTE",
JSON.stringify(t.derivePalette("not-a-color")) === JSON.stringify(t.DEFAULT_PALETTE));
}
document.getElementById("assertions").innerHTML = rows.map(function (r) { return "<div>" + r + "</div>"; }).join("");
}
/* 初始嵌入:打开页面即有挂件 + 断言跑一轮 */
document.addEventListener("DOMContentLoaded", function () { rebuildWidget({}); });
</script>
</body>
</html>