feat: jc 引擎主线搬入 app/engine(315 测试基线全绿:320 去掉 admin 5 用例)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
// ---- 动态主题色 bootstrap(spec §3):?color= → 派生 → :root 变量 ----
|
||||
(function () {
|
||||
if (typeof ChatTheme === "undefined") return; // theme.js 加载失败→style.css :root 品牌蓝兜底
|
||||
var m = /(?:^|[?&])color=([^&]*)/.exec(location.search);
|
||||
function safeDecode(s) { try { return decodeURIComponent(s); } catch (e) { return ""; } }
|
||||
var color = m ? safeDecode(m[1]).trim() : "";
|
||||
var palette = ChatTheme.derivePalette(color || ChatTheme.DEFAULT_COLOR);
|
||||
var root = document.documentElement.style;
|
||||
root.setProperty("--w-primary", palette.primary);
|
||||
root.setProperty("--w-primary-rgb", palette.primaryRgb);
|
||||
root.setProperty("--w-gradient-end", palette.gradientEnd);
|
||||
root.setProperty("--w-deep-text", palette.deepText);
|
||||
root.setProperty("--w-caption-text", palette.captionText);
|
||||
root.setProperty("--w-soft-bg", palette.softBg);
|
||||
root.setProperty("--w-border-soft", palette.borderSoft);
|
||||
|
||||
// 头像兜底(与挂件同机制)
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".avatar-img"), function (img) {
|
||||
function hideEmoji() {
|
||||
var s = img.parentElement && img.parentElement.querySelector(".avatar-emoji");
|
||||
if (s) s.style.display = "none";
|
||||
}
|
||||
if (img.complete && img.naturalWidth > 0) { hideEmoji(); return; }
|
||||
img.addEventListener("load", hideEmoji);
|
||||
img.addEventListener("error", function () { img.remove(); });
|
||||
});
|
||||
})();
|
||||
|
||||
(function () {
|
||||
// 联系方式配置(与 widget.js 的 data-* 机制一致)
|
||||
var chatScript = document.currentScript || document.querySelector('script[src*="chat.js"]');
|
||||
var whatsapp = (chatScript && chatScript.getAttribute("data-whatsapp")) || "https://wa.me/+8615937108317";
|
||||
var email = (chatScript && chatScript.getAttribute("data-email")) || "mailto:hesu@hesuu.com";
|
||||
var waBtn = document.querySelector('.whatsapp-btn');
|
||||
var emBtn = document.querySelector('.email-btn');
|
||||
if (waBtn) waBtn.href = whatsapp;
|
||||
if (emBtn) emBtn.href = email;
|
||||
var sessionId = localStorage.getItem("cc_session_id");
|
||||
var messagesEl = document.getElementById("messages");
|
||||
var inputEl = document.getElementById("input");
|
||||
var sendBtn = document.getElementById("sendBtn");
|
||||
var typingEl = document.getElementById("typing");
|
||||
var endedEl = document.getElementById("chatEnded");
|
||||
var popularTopicsEl = document.getElementById("popular-topics");
|
||||
var ws = null;
|
||||
var ended = false;
|
||||
|
||||
// Current streaming message element
|
||||
var streamBubble = null;
|
||||
var statusTimer = null;
|
||||
|
||||
function getWsUrl() {
|
||||
var loc = window.location;
|
||||
var proto = loc.protocol === "https:" ? "wss:" : "ws:";
|
||||
return proto + "//" + loc.host + "/ws/chat";
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(getWsUrl());
|
||||
|
||||
ws.onopen = function () {
|
||||
if (sessionId) {
|
||||
ws.send(JSON.stringify({ session_id: sessionId }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ source: "standalone" }));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = function (e) {
|
||||
var data = JSON.parse(e.data);
|
||||
|
||||
if (data.type === "init" || data.type === "resumed") {
|
||||
sessionId = data.session_id;
|
||||
localStorage.setItem("cc_session_id", sessionId);
|
||||
if (data.type === "resumed") {
|
||||
loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === "stream_start") {
|
||||
clearStatusTimer();
|
||||
hideTyping();
|
||||
streamBubble = addStreamBubble();
|
||||
}
|
||||
|
||||
if (data.type === "stream_chunk") {
|
||||
if (streamBubble) {
|
||||
streamBubble.textContent += data.content;
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === "stream_end") {
|
||||
streamBubble = null;
|
||||
}
|
||||
|
||||
if (data.type === "evaluated") {
|
||||
if (data.ended) {
|
||||
ended = true;
|
||||
endedEl.classList.add("active");
|
||||
inputEl.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === "error") {
|
||||
clearStatusTimer();
|
||||
hideTyping();
|
||||
streamBubble = null;
|
||||
addMessage("assistant", "抱歉,出了点问题,请稍后再试。");
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function () {
|
||||
if (!ended) {
|
||||
setTimeout(connect, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
function send() {
|
||||
var text = inputEl.value.trim();
|
||||
if (!text || ended || !ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
addMessage("user", text);
|
||||
inputEl.value = "";
|
||||
hidePopularTopics();
|
||||
|
||||
// Immediate feedback: show status then typing
|
||||
showStatusThenTyping();
|
||||
|
||||
ws.send(JSON.stringify({ content: text }));
|
||||
}
|
||||
|
||||
function sendTopicMessage(topic) {
|
||||
if (ended || !ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
addMessage("user", topic);
|
||||
hidePopularTopics();
|
||||
|
||||
// Immediate feedback: show status then typing
|
||||
showStatusThenTyping();
|
||||
|
||||
ws.send(JSON.stringify({ content: topic }));
|
||||
}
|
||||
|
||||
function hidePopularTopics() {
|
||||
if (popularTopicsEl) {
|
||||
popularTopicsEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showStatusThenTyping() {
|
||||
clearStatusTimer();
|
||||
showTyping();
|
||||
// Auto-hide typing is handled by stream_start replacing it
|
||||
}
|
||||
|
||||
function clearStatusTimer() {
|
||||
if (statusTimer) {
|
||||
clearTimeout(statusTimer);
|
||||
statusTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(role, content) {
|
||||
var div = document.createElement("div");
|
||||
div.className = "message " + role;
|
||||
|
||||
var wrapper = document.createElement("div");
|
||||
var bubble = document.createElement("div");
|
||||
bubble.className = "bubble";
|
||||
bubble.textContent = content;
|
||||
|
||||
wrapper.appendChild(bubble);
|
||||
div.appendChild(wrapper);
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
function addStreamBubble() {
|
||||
var div = document.createElement("div");
|
||||
div.className = "message assistant";
|
||||
|
||||
var wrapper = document.createElement("div");
|
||||
var bubble = document.createElement("div");
|
||||
bubble.className = "bubble";
|
||||
bubble.textContent = "";
|
||||
|
||||
wrapper.appendChild(bubble);
|
||||
div.appendChild(wrapper);
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
return bubble;
|
||||
}
|
||||
|
||||
function showTyping() {
|
||||
typingEl.classList.add("active");
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
function hideTyping() {
|
||||
typingEl.classList.remove("active");
|
||||
}
|
||||
|
||||
function loadHistory() {
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", send);
|
||||
inputEl.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
send();
|
||||
}
|
||||
});
|
||||
|
||||
// Popular topics buttons
|
||||
var topicBtns = document.querySelectorAll(".popular-topic-btn");
|
||||
topicBtns.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var topic = this.getAttribute("data-topic");
|
||||
sendTopicMessage(topic);
|
||||
});
|
||||
});
|
||||
|
||||
connect();
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
/* 客服主题派生——单品牌色 → 7 槽位色板(spec 2026-08-31 §5)。
|
||||
* 注意:此文件与 static/widget.js 内联拷贝必须逐字符一致(空白归一化后),
|
||||
* 由 tests/test_theme_static.py 强制;修改务必两边同步。 */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_COLOR = "#066fc0";
|
||||
|
||||
var DEFAULT_PALETTE = {
|
||||
primary: "#066fc0",
|
||||
primaryRgb: "6,111,192",
|
||||
gradientEnd: "#0666bb",
|
||||
deepText: "#14517a",
|
||||
captionText: "#2174ab",
|
||||
softBg: "#f3f9fe",
|
||||
borderSoft: "#bbdcf2"
|
||||
};
|
||||
|
||||
function clampNum(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
hex = String(hex || "").replace(/^#/, "");
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split("").map(function (ch) { return ch + ch; }).join("");
|
||||
}
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16)
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
var l = (max + min) / 2;
|
||||
var d = max - min;
|
||||
var h = 0, s = 0;
|
||||
if (d !== 0) {
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
if (max === r) h = ((g - b) / d) % 6;
|
||||
else if (max === g) h = (b - r) / d + 2;
|
||||
else h = (r - g) / d + 4;
|
||||
h *= 60;
|
||||
if (h < 0) h += 360;
|
||||
}
|
||||
return { h: h, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
function toHex2(v) {
|
||||
var s = Math.round(clampNum(v, 0, 255)).toString(16);
|
||||
return s.length < 2 ? "0" + s : s;
|
||||
}
|
||||
|
||||
function hslToHex(h, s, l) {
|
||||
h = ((h % 360) + 360) % 360;
|
||||
s = clampNum(s, 0, 100) / 100;
|
||||
l = clampNum(l, 0, 100) / 100;
|
||||
var c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
var x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
var m = l - c / 2;
|
||||
var rp = 0, gp = 0, bp = 0;
|
||||
if (h < 60) { rp = c; gp = x; }
|
||||
else if (h < 120) { rp = x; gp = c; }
|
||||
else if (h < 180) { gp = c; bp = x; }
|
||||
else if (h < 240) { gp = x; bp = c; }
|
||||
else if (h < 300) { rp = x; bp = c; }
|
||||
else { rp = c; bp = x; }
|
||||
return "#" + toHex2((rp + m) * 255) + toHex2((gp + m) * 255) + toHex2((bp + m) * 255);
|
||||
}
|
||||
|
||||
function derivePalette(color) {
|
||||
try {
|
||||
var rgb = hexToRgb(color);
|
||||
if (!rgb) {
|
||||
if (typeof console !== "undefined" && console.warn) console.warn("[chat-theme] invalid color, fallback:", color);
|
||||
return DEFAULT_PALETTE;
|
||||
}
|
||||
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
var h = hsl.h, s = hsl.s, l = hsl.l;
|
||||
return {
|
||||
primary: "#" + toHex2(rgb.r) + toHex2(rgb.g) + toHex2(rgb.b),
|
||||
primaryRgb: rgb.r + "," + rgb.g + "," + rgb.b,
|
||||
gradientEnd: hslToHex(h + 2, s, l - 1),
|
||||
deepText: hslToHex(h - 2, s * 0.76, clampNum(l - 24, 28, 40)),
|
||||
captionText: hslToHex(h - 2, s * 0.72, clampNum(l - 10, 40, 52)),
|
||||
softBg: hslToHex(h, s * 0.9, 97.5),
|
||||
borderSoft: hslToHex(h - 2, s * 0.72, 84)
|
||||
};
|
||||
} catch (e) {
|
||||
return DEFAULT_PALETTE;
|
||||
}
|
||||
}
|
||||
|
||||
global.ChatTheme = {
|
||||
derivePalette: derivePalette,
|
||||
DEFAULT_COLOR: DEFAULT_COLOR,
|
||||
DEFAULT_PALETTE: DEFAULT_PALETTE
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
Reference in New Issue
Block a user