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();
|
||||
})();
|
||||
Reference in New Issue
Block a user