Browse Source

添加窗口缩放功能和更换入口图标

dev-mcp
wanghanlin 2 weeks ago
parent
commit
215965efc2
  1. 375
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 21
      client/src/config.ts
  6. 119
      client/src/dom.ts
  7. 2
      client/src/i18n.ts
  8. 44
      client/src/index.ts
  9. 192
      client/src/styles.ts
  10. 4
      client/src/types.ts
  11. 375
      src/main/resources/static/sdk/chatbot-sdk.js
  12. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  13. 56
      src/main/resources/static/sdk/test.html

375
client/dist/chatbot-sdk.js

@ -96,24 +96,8 @@ var ChatbotSDK = (function () {
}, },
}; };
/** 默认悬浮按钮 SVG 图标(赛博机器人:对话气泡 + 机器人面孔 + 天线能量灯 + 眨眼动效) */
const DEFAULT_LAUNCHER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="csk-ico-grad" x1="6" y1="8" x2="26" y2="28" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#fff" stop-opacity="1"/>
<stop offset="100%" stop-color="#fff" stop-opacity="0.82"/>
</linearGradient>
</defs>
<rect x="5" y="8" width="22" height="16" rx="4" fill="url(#csk-ico-grad)"/>
<path d="M9 24L6 28.5l6-4.5z" fill="url(#csk-ico-grad)"/>
<line x1="16" y1="8" x2="16" y2="4" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="16" cy="3" r="1.5" fill="#fff" class="csk-ico-antenna"/>
<rect x="9.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<rect x="18.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<circle cx="11" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<circle cx="20" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<path d="M12.5 20 Q16 22.5 19.5 20" stroke="rgba(99,102,241,0.4)" stroke-width="1.2" fill="none" stroke-linecap="round"/>
</svg>`;
/** 默认悬浮按钮图标 — C3 三点脉冲序列:极光旋转渐变底 + 三颗顺序脉冲白点 */
const DEFAULT_LAUNCHER_ICON = `<span class="csk-ico-pulse-dot csk-ico-pulse-dot--1"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--2"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--3"></span>`;
// ==================== 马卡龙色系玻璃质感图标 ==================== // ==================== 马卡龙色系玻璃质感图标 ====================
/** /**
* 生成玻璃质感机器人 SVG 图标 * 生成玻璃质感机器人 SVG 图标
@ -186,7 +170,7 @@ var ChatbotSDK = (function () {
* 解析并校验用户传入的配置填充默认值 * 解析并校验用户传入的配置填充默认值
*/ */
function parseConfig(raw) { function parseConfig(raw) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
// 校验必传参数:integrateId(对应后端 roleId) // 校验必传参数:integrateId(对应后端 roleId)
if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number') if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number')
|| (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) { || (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) {
@ -202,7 +186,7 @@ var ChatbotSDK = (function () {
try { try {
new URL(raw.requestDomain); new URL(raw.requestDomain);
} }
catch (_m) {
catch (_o) {
logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`);
return null; return null;
} }
@ -248,13 +232,14 @@ var ChatbotSDK = (function () {
theme: raw.theme === 'dark' ? 'dark' : 'light', theme: raw.theme === 'dark' ? 'dark' : 'light',
showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true, showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true,
enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : true,
resizable: (_g = raw.resizable) !== null && _g !== void 0 ? _g : true,
streaming: (_h = raw.streaming) !== null && _h !== void 0 ? _h : true,
enableRag: (_j = raw.enableRag) !== null && _j !== void 0 ? _j : true,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE', rewriteStrategy: raw.rewriteStrategy || 'REWRITE',
locale: raw.locale || 'zh-CN', locale: raw.locale || 'zh-CN',
debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true,
sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false,
notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false,
debug: (_k = raw.debug) !== null && _k !== void 0 ? _k : true,
sound: (_l = raw.sound) !== null && _l !== void 0 ? _l : false,
notification: (_m = raw.notification) !== null && _m !== void 0 ? _m : false,
onError: typeof raw.onError === 'function' ? raw.onError : undefined, onError: typeof raw.onError === 'function' ? raw.onError : undefined,
onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined, onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined,
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
@ -322,6 +307,7 @@ var ChatbotSDK = (function () {
// 提示气泡 // 提示气泡
teaser_text: '有什么可以帮你的吗?', teaser_text: '有什么可以帮你的吗?',
new_msg_announce: '收到新消息', new_msg_announce: '收到新消息',
resize: '拖拽缩放窗口',
// 错误提示 // 错误提示
error_network: '网络连接失败,请检查网络', error_network: '网络连接失败,请检查网络',
error_timeout: '请求超时,请稍后重试', error_timeout: '请求超时,请稍后重试',
@ -387,6 +373,7 @@ var ChatbotSDK = (function () {
// Teaser // Teaser
teaser_text: 'How can I help you?', teaser_text: 'How can I help you?',
new_msg_announce: 'New message received', new_msg_announce: 'New message received',
resize: 'Resize window',
// Errors // Errors
error_network: 'Network connection failed', error_network: 'Network connection failed',
error_timeout: 'Request timed out, please try again', error_timeout: 'Request timed out, please try again',
@ -1063,7 +1050,13 @@ var ChatbotSDK = (function () {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
background: var(--csk-bg-user);
/* 极光旋转渐变底 — 默认蓝紫系,由 primaryColor 驱动的 conic-gradient */
background: conic-gradient(from 0deg,
var(--csk-primary-light, #06b6d4),
var(--csk-primary, #3b82f6),
var(--csk-primary-hover, #8b5cf6),
var(--csk-primary-light, #06b6d4)
);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1083,7 +1076,9 @@ var ChatbotSDK = (function () {
transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.25s ease, box-shadow 0.25s ease,
border-color 0.25s ease; border-color 0.25s ease;
animation: csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation:
csk-aurora-spin 4s linear infinite,
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.csk-launcher--right { right: 24px; } .csk-launcher--right { right: 24px; }
.csk-launcher--left { left: 24px; } .csk-launcher--left { left: 24px; }
@ -1147,6 +1142,64 @@ var ChatbotSDK = (function () {
60% { transform: scale(1.06) translateY(-2px); } 60% { transform: scale(1.06) translateY(-2px); }
100% { opacity: 1; transform: scale(1) translateY(0); } 100% { opacity: 1; transform: scale(1) translateY(0); }
} }
/* ========== 三点脉冲序列图标 ========== */
/* 极光旋转 */
@keyframes csk-aurora-spin {
to { transform: rotate(360deg); }
}
/* 脉冲点 */
.csk-ico-pulse-dot {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background: #fff;
z-index: 5;
animation: csk-seq-pulse 1.2s ease-in-out infinite;
box-shadow: 0 0 6px 1px rgba(255, 255, 255, 0.5);
}
.csk-ico-pulse-dot--1 { top: 4px; left: 50%; margin-left: -3px; }
.csk-ico-pulse-dot--2 { top: 50%; right: 5px; margin-top: -3px; animation-delay: 0.3s; }
.csk-ico-pulse-dot--3 { bottom: 5px; left: 50%; margin-left: -3px; animation-delay: 0.6s; }
@keyframes csk-seq-pulse {
0%, 100% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
35% { transform: scale(1.8); opacity: 1; box-shadow: 0 0 12px 4px rgba(255, 255, 255, 0.8); }
70% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
}
/* 移除旧的 SVG 图标相关样式(已不再使用) */
/* ========== 高光扫过动效 ========== */
.csk-launcher::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 40%;
height: 200%;
border-radius: 50%;
background: linear-gradient(
105deg,
transparent 30%,
rgba(255, 255, 255, 0.28) 48%,
rgba(255, 255, 255, 0.1) 52%,
transparent 70%
);
transform: rotate(25deg);
animation: csk-shimmer 4s ease-in-out infinite;
pointer-events: none;
}
/* 呼吸色晕 — 配合接地阴影,强化白底上的立体存在感 */
.csk-launcher::after {
content: '';
position: absolute;
inset: -18px;
border-radius: 50%;
background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.18) 0%, rgba(var(--csk-primary-rgb), 0.05) 45%, transparent 68%);
animation: csk-breathe 3.5s ease-in-out infinite;
pointer-events: none;
}
@keyframes csk-shimmer { @keyframes csk-shimmer {
0%, 100% { left: -60%; opacity: 0; } 0%, 100% { left: -60%; opacity: 0; }
20% { opacity: 1; } 20% { opacity: 1; }
@ -1158,25 +1211,11 @@ var ChatbotSDK = (function () {
50% { transform: scale(1.15); opacity: 0.35; } 50% { transform: scale(1.15); opacity: 0.35; }
} }
/* SVG 图标内部动效:眨眼 */
.csk-launcher .csk-ico-eye {
animation: csk-eye-blink 4s ease-in-out infinite;
}
@keyframes csk-eye-blink {
0%, 42%, 48%, 100% { opacity: 1; }
45% { opacity: 0.1; }
}
/* SVG 图标内部动效:天线能量闪烁 */
.csk-launcher .csk-ico-antenna {
animation: csk-antenna-glow 2s ease-in-out infinite;
}
@keyframes csk-antenna-glow {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* ========== 磨砂玻璃质感悬浮按钮 ========== */ /* ========== 磨砂玻璃质感悬浮按钮 ========== */
.csk-launcher--glass { .csk-launcher--glass {
/* 覆盖旋转动画为静止渐变 */
animation:
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both !important;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--csk-launcher-grad-1, var(--csk-primary)) 0%, var(--csk-launcher-grad-1, var(--csk-primary)) 0%,
@ -1237,18 +1276,7 @@ var ChatbotSDK = (function () {
70% { left: 130%; opacity: 0; } 70% { left: 130%; opacity: 0; }
} }
/* SVG 内部动效:跳动气泡(3 个小圆点交错弹跳) */
.csk-launcher .csk-ico-bubble {
animation: csk-bubble-jump 1.4s ease-in-out infinite;
}
.csk-launcher .csk-ico-bubble--1 { animation-delay: 0s; }
.csk-launcher .csk-ico-bubble--2 { animation-delay: 0.18s; }
.csk-launcher .csk-ico-bubble--3 { animation-delay: 0.36s; }
@keyframes csk-bubble-jump {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-3.5px); }
60% { transform: translateY(-1px); }
}
/* 三点脉冲图标在玻璃主题下保持可见 */
/* ========== 聊天弹窗 ========== */ /* ========== 聊天弹窗 ========== */
.csk-window { .csk-window {
@ -2105,6 +2133,94 @@ var ChatbotSDK = (function () {
box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important;
} }
/* ========== 窗口缩放拖拽手柄 ========== */
.csk-resize-handle {
position: absolute;
right: 4px;
bottom: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: flex-end;
justify-content: flex-end;
cursor: nwse-resize;
z-index: 5;
user-select: none;
-webkit-user-select: none;
/* 圆形半透明背景,与整体圆润风格一致 */
border-radius: 50%;
background: transparent;
transition: background 0.2s ease;
}
.csk-resize-handle:hover {
background: rgba(var(--csk-primary-rgb), 0.08);
}
/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */
.csk-resize-grip {
display: block;
width: 10px;
height: 10px;
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#C8CCD2 38%, #C8CCD2 42%,
transparent 42%, transparent 54%,
#C8CCD2 54%, #C8CCD2 58%,
transparent 58%, transparent 70%,
#C8CCD2 70%, #C8CCD2 74%,
transparent 74%
);
transition: background 0.2s ease;
border-radius: 2px;
}
/* hover 时线条变为主色 */
.csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary) 38%, var(--csk-primary) 42%,
transparent 42%, transparent 54%,
var(--csk-primary) 54%, var(--csk-primary) 58%,
transparent 58%, transparent 70%,
var(--csk-primary) 70%, var(--csk-primary) 74%,
transparent 74%
);
}
/* 暗色模式 */
.csk-dark .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#5B6070 38%, #5B6070 42%,
transparent 42%, transparent 54%,
#5B6070 54%, #5B6070 58%,
transparent 58%, transparent 70%,
#5B6070 70%, #5B6070 74%,
transparent 74%
);
}
.csk-dark .csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary-light) 38%, var(--csk-primary-light) 42%,
transparent 42%, transparent 54%,
var(--csk-primary-light) 54%, var(--csk-primary-light) 58%,
transparent 58%, transparent 70%,
var(--csk-primary-light) 70%, var(--csk-primary-light) 74%,
transparent 74%
);
}
/* 缩放拖拽中:禁用过渡动画 + 全局光标 */
.csk-window--resizing {
transition: none !important;
user-select: none;
}
.csk-window--resizing * { cursor: nwse-resize !important; }
/* ========== 暗色模式 ========== */ /* ========== 暗色模式 ========== */
.csk-root.csk-dark { .csk-root.csk-dark {
--csk-bg-ai: #1E1E2E; --csk-bg-ai: #1E1E2E;
@ -2372,6 +2488,7 @@ var ChatbotSDK = (function () {
left: 0 !important; left: 0 !important;
border-radius: 0; border-radius: 0;
} }
.csk-resize-handle { display: none !important; }
.csk-window--hidden { transform: translateY(100%); } .csk-window--hidden { transform: translateY(100%); }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; } .csk-launcher:not([style*="bottom"]) { bottom: 20px; }
@ -2719,6 +2836,13 @@ var ChatbotSDK = (function () {
<button class="csk-teaser__close" aria-label="${t('close')}" type="button">&times;</button> <button class="csk-teaser__close" aria-label="${t('close')}" type="button">&times;</button>
<span>${config.teaserText || t('teaser_text')}</span> <span>${config.teaserText || t('teaser_text')}</span>
`; `;
// === 缩放拖拽手柄(右下角) ===
const resizeHandle = document.createElement('div');
resizeHandle.className = 'csk-resize-handle';
resizeHandle.setAttribute('aria-label', t('resize'));
// 纯 CSS 渲染三条斜线,无需 SVG
resizeHandle.innerHTML = '<span class="csk-resize-grip"></span>';
windowEl.appendChild(resizeHandle);
return { return {
window: windowEl, window: windowEl,
messagesContainer, messagesContainer,
@ -2730,6 +2854,7 @@ var ChatbotSDK = (function () {
welcomeEl, welcomeEl,
newMsgBtn, newMsgBtn,
teaserEl, teaserEl,
resizeHandle,
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
showLoading, showLoading,
@ -2939,6 +3064,100 @@ var ChatbotSDK = (function () {
document.removeEventListener('touchend', onTouchEnd); document.removeEventListener('touchend', onTouchEnd);
}; };
} }
// ==================== 窗口缩放支持 ====================
/** 窗口最小尺寸 */
const MIN_WIDTH = 300;
const MIN_HEIGHT = 300;
/**
* 启用窗口右下角拖拽缩放
* @param handleEl 缩放手柄元素
* @param windowEl 聊天窗口元素
* @param onResizeEnd 缩放结束回调返回最终宽高用于持久化
* @returns 清理函数
*/
function enableResize(handleEl, windowEl, onResizeEnd) {
let resizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
const onPointerDown = (clientX, clientY) => {
resizing = true;
startX = clientX;
startY = clientY;
const rect = windowEl.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
windowEl.classList.add('csk-window--resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
};
const onPointerMove = (clientX, clientY, e) => {
if (!resizing)
return;
if (e)
e.preventDefault();
const dx = clientX - startX;
const dy = clientY - startY;
const newWidth = Math.max(MIN_WIDTH, Math.min(startWidth + dx, window.innerWidth - 24));
const newHeight = Math.max(MIN_HEIGHT, Math.min(startHeight + dy, window.innerHeight - 24));
windowEl.style.width = `${newWidth}px`;
windowEl.style.height = `${newHeight}px`;
// 同步更新 CSS 变量(供内部布局使用)
windowEl.style.setProperty('--csk-window-width', `${newWidth}px`);
windowEl.style.setProperty('--csk-window-height', `${newHeight}px`);
// 拖拽缩放后清除 bottom/right 定位,避免与固定定位冲突
windowEl.style.bottom = '';
windowEl.style.right = '';
};
const onPointerUp = () => {
if (!resizing)
return;
resizing = false;
windowEl.classList.remove('csk-window--resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
const rect = windowEl.getBoundingClientRect();
if (onResizeEnd) {
onResizeEnd({ width: rect.width, height: rect.height });
}
};
// 鼠标事件(DOM 事件监听器包装)
function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp() { onPointerUp(); }
// 鼠标事件
function onMouseDown(e) {
e.preventDefault();
e.stopPropagation();
onPointerDown(e.clientX, e.clientY);
}
// 触摸事件
function onTouchStart(e) {
e.preventDefault();
e.stopPropagation();
if (e.touches.length === 1)
onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e) {
if (e.touches.length === 1)
onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd() { onPointerUp(); }
handleEl.addEventListener('mousedown', onMouseDown);
handleEl.addEventListener('touchstart', onTouchStart, { passive: false });
return () => {
handleEl.removeEventListener('mousedown', onMouseDown);
handleEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ==================== // ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */ /** 渲染用户消息气泡 */
function renderUserBubble(container, text, timestamp) { function renderUserBubble(container, text, timestamp) {
@ -4388,6 +4607,7 @@ var ChatbotSDK = (function () {
let hideLoadingFn = null; let hideLoadingFn = null;
let dragCleanup = null; let dragCleanup = null;
let launcherDragCleanup = null; let launcherDragCleanup = null;
let resizeCleanup = null;
/** Launcher 位置记忆状态(拖拽后更新) */ /** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos = null; let launcherPos = null;
/** 提示气泡元素 */ /** 提示气泡元素 */
@ -4468,6 +4688,15 @@ var ChatbotSDK = (function () {
} }
// 9.1 恢复上次拖拽位置 // 9.1 恢复上次拖拽位置
restoreWindowPosition(config.integrateId, windowEl); restoreWindowPosition(config.integrateId, windowEl);
// 9.2 启用缩放(resizable 配置控制)
if (config.resizable) {
const resizeHandle = dom.resizeHandle;
resizeCleanup = enableResize(resizeHandle, windowEl, (size) => {
saveWindowSize(config.integrateId, size);
});
// 恢复上次缩放的尺寸
restoreWindowSize(config.integrateId, windowEl);
}
// 10. 初始化对话模块 // 10. 初始化对话模块
initChat(config, { initChat(config, {
messagesContainer, messagesContainer,
@ -4686,6 +4915,10 @@ var ChatbotSDK = (function () {
launcherDragCleanup(); launcherDragCleanup();
launcherDragCleanup = null; launcherDragCleanup = null;
} }
if (resizeCleanup) {
resizeCleanup();
resizeCleanup = null;
}
launcherPos = null; launcherPos = null;
removeStyles(); removeStyles();
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDown);
@ -4741,6 +4974,36 @@ var ChatbotSDK = (function () {
clearMessages(config.integrateId); clearMessages(config.integrateId);
} }
} }
// ==================== 窗口尺寸记忆 ====================
/** localStorage key:窗口尺寸 */
function sizeKey(integrateId) {
return `csk_size_${integrateId}`;
}
/** 保存窗口缩放的尺寸到 localStorage */
function saveWindowSize(integrateId, size) {
try {
localStorage.setItem(sizeKey(integrateId), JSON.stringify(size));
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 恢复窗口缩放尺寸 */
function restoreWindowSize(integrateId, winEl) {
try {
const raw = localStorage.getItem(sizeKey(integrateId));
if (!raw)
return;
const size = JSON.parse(raw);
if (typeof size.width !== 'number' || typeof size.height !== 'number')
return;
const w = Math.max(300, Math.min(size.width, window.innerWidth - 24));
const h = Math.max(300, Math.min(size.height, window.innerHeight - 24));
winEl.style.width = `${w}px`;
winEl.style.height = `${h}px`;
winEl.style.setProperty('--csk-window-width', `${w}px`);
winEl.style.setProperty('--csk-window-height', `${h}px`);
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
// ==================== 窗口位置记忆 ==================== // ==================== 窗口位置记忆 ====================
/** localStorage key:窗口位置 */ /** localStorage key:窗口位置 */
function positionKey(integrateId) { function positionKey(integrateId) {

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

21
client/src/config.ts

@ -9,24 +9,8 @@
import { SDKConfig, ResolvedConfig } from './types'; import { SDKConfig, ResolvedConfig } from './types';
import { logger } from './logger'; import { logger } from './logger';
/** 默认悬浮按钮 SVG 图标(赛博机器人:对话气泡 + 机器人面孔 + 天线能量灯 + 眨眼动效) */
const DEFAULT_LAUNCHER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="csk-ico-grad" x1="6" y1="8" x2="26" y2="28" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#fff" stop-opacity="1"/>
<stop offset="100%" stop-color="#fff" stop-opacity="0.82"/>
</linearGradient>
</defs>
<rect x="5" y="8" width="22" height="16" rx="4" fill="url(#csk-ico-grad)"/>
<path d="M9 24L6 28.5l6-4.5z" fill="url(#csk-ico-grad)"/>
<line x1="16" y1="8" x2="16" y2="4" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="16" cy="3" r="1.5" fill="#fff" class="csk-ico-antenna"/>
<rect x="9.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<rect x="18.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<circle cx="11" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<circle cx="20" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<path d="M12.5 20 Q16 22.5 19.5 20" stroke="rgba(99,102,241,0.4)" stroke-width="1.2" fill="none" stroke-linecap="round"/>
</svg>`;
/** 默认悬浮按钮图标 — C3 三点脉冲序列:极光旋转渐变底 + 三颗顺序脉冲白点 */
const DEFAULT_LAUNCHER_ICON = `<span class="csk-ico-pulse-dot csk-ico-pulse-dot--1"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--2"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--3"></span>`;
// ==================== 马卡龙色系玻璃质感图标 ==================== // ==================== 马卡龙色系玻璃质感图标 ====================
@ -178,6 +162,7 @@ export function parseConfig(raw: SDKConfig): ResolvedConfig | null {
theme: raw.theme === 'dark' ? 'dark' : 'light', theme: raw.theme === 'dark' ? 'dark' : 'light',
showTeaser: raw.showTeaser ?? true, showTeaser: raw.showTeaser ?? true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
resizable: raw.resizable ?? true,
streaming: raw.streaming ?? true, streaming: raw.streaming ?? true,
enableRag: raw.enableRag ?? true, enableRag: raw.enableRag ?? true,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE', rewriteStrategy: raw.rewriteStrategy || 'REWRITE',

119
client/src/dom.ts

@ -70,6 +70,7 @@ export function createChatWindow(config: ResolvedConfig): {
welcomeEl: HTMLElement; welcomeEl: HTMLElement;
newMsgBtn: HTMLElement; newMsgBtn: HTMLElement;
teaserEl: HTMLElement; teaserEl: HTMLElement;
resizeHandle: HTMLElement;
searchInput: HTMLInputElement | null; searchInput: HTMLInputElement | null;
ariaLiveEl: HTMLElement; ariaLiveEl: HTMLElement;
showLoading: () => HTMLElement; showLoading: () => HTMLElement;
@ -345,6 +346,14 @@ export function createChatWindow(config: ResolvedConfig): {
<span>${config.teaserText || t('teaser_text')}</span> <span>${config.teaserText || t('teaser_text')}</span>
`; `;
// === 缩放拖拽手柄(右下角) ===
const resizeHandle = document.createElement('div');
resizeHandle.className = 'csk-resize-handle';
resizeHandle.setAttribute('aria-label', t('resize'));
// 纯 CSS 渲染三条斜线,无需 SVG
resizeHandle.innerHTML = '<span class="csk-resize-grip"></span>';
windowEl.appendChild(resizeHandle);
return { return {
window: windowEl, window: windowEl,
messagesContainer, messagesContainer,
@ -356,6 +365,7 @@ export function createChatWindow(config: ResolvedConfig): {
welcomeEl, welcomeEl,
newMsgBtn, newMsgBtn,
teaserEl, teaserEl,
resizeHandle,
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
showLoading, showLoading,
@ -586,6 +596,115 @@ export function enableLauncherDrag(
}; };
} }
// ==================== 窗口缩放支持 ====================
/** 窗口最小尺寸 */
const MIN_WIDTH = 300;
const MIN_HEIGHT = 300;
/**
*
* @param handleEl
* @param windowEl
* @param onResizeEnd
* @returns
*/
export function enableResize(
handleEl: HTMLElement,
windowEl: HTMLElement,
onResizeEnd?: (size: { width: number; height: number }) => void,
): () => void {
let resizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
const onPointerDown = (clientX: number, clientY: number): void => {
resizing = true;
startX = clientX;
startY = clientY;
const rect = windowEl.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
windowEl.classList.add('csk-window--resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
};
const onPointerMove = (clientX: number, clientY: number, e?: Event): void => {
if (!resizing) return;
if (e) e.preventDefault();
const dx = clientX - startX;
const dy = clientY - startY;
const newWidth = Math.max(MIN_WIDTH, Math.min(startWidth + dx, window.innerWidth - 24));
const newHeight = Math.max(MIN_HEIGHT, Math.min(startHeight + dy, window.innerHeight - 24));
windowEl.style.width = `${newWidth}px`;
windowEl.style.height = `${newHeight}px`;
// 同步更新 CSS 变量(供内部布局使用)
windowEl.style.setProperty('--csk-window-width', `${newWidth}px`);
windowEl.style.setProperty('--csk-window-height', `${newHeight}px`);
// 拖拽缩放后清除 bottom/right 定位,避免与固定定位冲突
windowEl.style.bottom = '';
windowEl.style.right = '';
};
const onPointerUp = (): void => {
if (!resizing) return;
resizing = false;
windowEl.classList.remove('csk-window--resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
const rect = windowEl.getBoundingClientRect();
if (onResizeEnd) {
onResizeEnd({ width: rect.width, height: rect.height });
}
};
// 鼠标事件(DOM 事件监听器包装)
function onMouseMove(e: MouseEvent): void { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp(): void { onPointerUp(); }
// 鼠标事件
function onMouseDown(e: MouseEvent): void {
e.preventDefault();
e.stopPropagation();
onPointerDown(e.clientX, e.clientY);
}
// 触摸事件
function onTouchStart(e: TouchEvent): void {
e.preventDefault();
e.stopPropagation();
if (e.touches.length === 1) onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e: TouchEvent): void {
if (e.touches.length === 1) onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd(): void { onPointerUp(); }
handleEl.addEventListener('mousedown', onMouseDown);
handleEl.addEventListener('touchstart', onTouchStart, { passive: false });
return () => {
handleEl.removeEventListener('mousedown', onMouseDown);
handleEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ==================== // ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */ /** 渲染用户消息气泡 */

2
client/src/i18n.ts

@ -64,6 +64,7 @@ const dictionaries: Record<string, Record<string, string>> = {
// 提示气泡 // 提示气泡
teaser_text: '有什么可以帮你的吗?', teaser_text: '有什么可以帮你的吗?',
new_msg_announce: '收到新消息', new_msg_announce: '收到新消息',
resize: '拖拽缩放窗口',
// 错误提示 // 错误提示
error_network: '网络连接失败,请检查网络', error_network: '网络连接失败,请检查网络',
@ -140,6 +141,7 @@ const dictionaries: Record<string, Record<string, string>> = {
// Teaser // Teaser
teaser_text: 'How can I help you?', teaser_text: 'How can I help you?',
new_msg_announce: 'New message received', new_msg_announce: 'New message received',
resize: 'Resize window',
// Errors // Errors
error_network: 'Network connection failed', error_network: 'Network connection failed',

44
client/src/index.ts

@ -12,7 +12,7 @@ import { parseConfig } from './config';
import { setDebug, logger, setErrorCallback } from './logger'; import { setDebug, logger, setErrorCallback } from './logger';
import { setApiConfig, clearApiConfig } from './api'; import { setApiConfig, clearApiConfig } from './api';
import { injectStyles, removeStyles } from './styles'; import { injectStyles, removeStyles } from './styles';
import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag } from './dom';
import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag, enableResize } from './dom';
import { initChat, initChatHistory, getMessages, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback } from './chat'; import { initChat, initChatHistory, getMessages, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback } from './chat';
import { clearMessages } from './storage'; import { clearMessages } from './storage';
import { setLocale } from './i18n'; import { setLocale } from './i18n';
@ -46,6 +46,7 @@ let showLoadingFn: (() => HTMLElement) | null = null;
let hideLoadingFn: (() => void) | null = null; let hideLoadingFn: (() => void) | null = null;
let dragCleanup: (() => void) | null = null; let dragCleanup: (() => void) | null = null;
let launcherDragCleanup: (() => void) | null = null; let launcherDragCleanup: (() => void) | null = null;
let resizeCleanup: (() => void) | null = null;
/** Launcher 位置记忆状态(拖拽后更新) */ /** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos: { side: 'left' | 'right'; bottom: number } | null = null; let launcherPos: { side: 'left' | 'right'; bottom: number } | null = null;
@ -146,6 +147,16 @@ function init(rawConfig: SDKConfig): void {
// 9.1 恢复上次拖拽位置 // 9.1 恢复上次拖拽位置
restoreWindowPosition(config.integrateId, windowEl); restoreWindowPosition(config.integrateId, windowEl);
// 9.2 启用缩放(resizable 配置控制)
if (config.resizable) {
const resizeHandle = dom.resizeHandle;
resizeCleanup = enableResize(resizeHandle, windowEl, (size) => {
saveWindowSize(config!.integrateId, size);
});
// 恢复上次缩放的尺寸
restoreWindowSize(config.integrateId, windowEl);
}
// 10. 初始化对话模块 // 10. 初始化对话模块
initChat(config, { initChat(config, {
messagesContainer, messagesContainer,
@ -358,6 +369,7 @@ function destroy(): void {
if (windowEl && windowEl.parentNode) { windowEl.parentNode.removeChild(windowEl); windowEl = null; } if (windowEl && windowEl.parentNode) { windowEl.parentNode.removeChild(windowEl); windowEl = null; }
if (dragCleanup) { dragCleanup(); dragCleanup = null; } if (dragCleanup) { dragCleanup(); dragCleanup = null; }
if (launcherDragCleanup) { launcherDragCleanup(); launcherDragCleanup = null; } if (launcherDragCleanup) { launcherDragCleanup(); launcherDragCleanup = null; }
if (resizeCleanup) { resizeCleanup(); resizeCleanup = null; }
launcherPos = null; launcherPos = null;
removeStyles(); removeStyles();
@ -411,6 +423,36 @@ function clearHistory(): void {
else if (confirm('确定清空所有对话记录?')) { clearMessages(config.integrateId); } else if (confirm('确定清空所有对话记录?')) { clearMessages(config.integrateId); }
} }
// ==================== 窗口尺寸记忆 ====================
/** localStorage key:窗口尺寸 */
function sizeKey(integrateId: string): string {
return `csk_size_${integrateId}`;
}
/** 保存窗口缩放的尺寸到 localStorage */
function saveWindowSize(integrateId: string, size: { width: number; height: number }): void {
try {
localStorage.setItem(sizeKey(integrateId), JSON.stringify(size));
} catch { /* 忽略 */ }
}
/** 恢复窗口缩放尺寸 */
function restoreWindowSize(integrateId: string, winEl: HTMLElement): void {
try {
const raw = localStorage.getItem(sizeKey(integrateId));
if (!raw) return;
const size = JSON.parse(raw) as { width: number; height: number };
if (typeof size.width !== 'number' || typeof size.height !== 'number') return;
const w = Math.max(300, Math.min(size.width, window.innerWidth - 24));
const h = Math.max(300, Math.min(size.height, window.innerHeight - 24));
winEl.style.width = `${w}px`;
winEl.style.height = `${h}px`;
winEl.style.setProperty('--csk-window-width', `${w}px`);
winEl.style.setProperty('--csk-window-height', `${h}px`);
} catch { /* 忽略 */ }
}
// ==================== 窗口位置记忆 ==================== // ==================== 窗口位置记忆 ====================
/** localStorage key:窗口位置 */ /** localStorage key:窗口位置 */

192
client/src/styles.ts

@ -92,7 +92,13 @@ function getStyles(config: ResolvedConfig): string {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
background: var(--csk-bg-user);
/* 极光旋转渐变底 — 默认蓝紫系,由 primaryColor 驱动的 conic-gradient */
background: conic-gradient(from 0deg,
var(--csk-primary-light, #06b6d4),
var(--csk-primary, #3b82f6),
var(--csk-primary-hover, #8b5cf6),
var(--csk-primary-light, #06b6d4)
);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -112,7 +118,9 @@ function getStyles(config: ResolvedConfig): string {
transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.25s ease, box-shadow 0.25s ease,
border-color 0.25s ease; border-color 0.25s ease;
animation: csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation:
csk-aurora-spin 4s linear infinite,
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.csk-launcher--right { right: 24px; } .csk-launcher--right { right: 24px; }
.csk-launcher--left { left: 24px; } .csk-launcher--left { left: 24px; }
@ -176,6 +184,64 @@ function getStyles(config: ResolvedConfig): string {
60% { transform: scale(1.06) translateY(-2px); } 60% { transform: scale(1.06) translateY(-2px); }
100% { opacity: 1; transform: scale(1) translateY(0); } 100% { opacity: 1; transform: scale(1) translateY(0); }
} }
/* ========== 三点脉冲序列图标 ========== */
/* 极光旋转 */
@keyframes csk-aurora-spin {
to { transform: rotate(360deg); }
}
/* 脉冲点 */
.csk-ico-pulse-dot {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background: #fff;
z-index: 5;
animation: csk-seq-pulse 1.2s ease-in-out infinite;
box-shadow: 0 0 6px 1px rgba(255, 255, 255, 0.5);
}
.csk-ico-pulse-dot--1 { top: 4px; left: 50%; margin-left: -3px; }
.csk-ico-pulse-dot--2 { top: 50%; right: 5px; margin-top: -3px; animation-delay: 0.3s; }
.csk-ico-pulse-dot--3 { bottom: 5px; left: 50%; margin-left: -3px; animation-delay: 0.6s; }
@keyframes csk-seq-pulse {
0%, 100% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
35% { transform: scale(1.8); opacity: 1; box-shadow: 0 0 12px 4px rgba(255, 255, 255, 0.8); }
70% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
}
/* 移除旧的 SVG 图标相关样式(已不再使用) */
/* ========== 高光扫过动效 ========== */
.csk-launcher::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 40%;
height: 200%;
border-radius: 50%;
background: linear-gradient(
105deg,
transparent 30%,
rgba(255, 255, 255, 0.28) 48%,
rgba(255, 255, 255, 0.1) 52%,
transparent 70%
);
transform: rotate(25deg);
animation: csk-shimmer 4s ease-in-out infinite;
pointer-events: none;
}
/* 呼吸色晕 — 配合接地阴影,强化白底上的立体存在感 */
.csk-launcher::after {
content: '';
position: absolute;
inset: -18px;
border-radius: 50%;
background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.18) 0%, rgba(var(--csk-primary-rgb), 0.05) 45%, transparent 68%);
animation: csk-breathe 3.5s ease-in-out infinite;
pointer-events: none;
}
@keyframes csk-shimmer { @keyframes csk-shimmer {
0%, 100% { left: -60%; opacity: 0; } 0%, 100% { left: -60%; opacity: 0; }
20% { opacity: 1; } 20% { opacity: 1; }
@ -187,25 +253,11 @@ function getStyles(config: ResolvedConfig): string {
50% { transform: scale(1.15); opacity: 0.35; } 50% { transform: scale(1.15); opacity: 0.35; }
} }
/* SVG 图标内部动效:眨眼 */
.csk-launcher .csk-ico-eye {
animation: csk-eye-blink 4s ease-in-out infinite;
}
@keyframes csk-eye-blink {
0%, 42%, 48%, 100% { opacity: 1; }
45% { opacity: 0.1; }
}
/* SVG 图标内部动效:天线能量闪烁 */
.csk-launcher .csk-ico-antenna {
animation: csk-antenna-glow 2s ease-in-out infinite;
}
@keyframes csk-antenna-glow {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* ========== 磨砂玻璃质感悬浮按钮 ========== */ /* ========== 磨砂玻璃质感悬浮按钮 ========== */
.csk-launcher--glass { .csk-launcher--glass {
/* 覆盖旋转动画为静止渐变 */
animation:
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both !important;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--csk-launcher-grad-1, var(--csk-primary)) 0%, var(--csk-launcher-grad-1, var(--csk-primary)) 0%,
@ -266,18 +318,7 @@ function getStyles(config: ResolvedConfig): string {
70% { left: 130%; opacity: 0; } 70% { left: 130%; opacity: 0; }
} }
/* SVG 内部动效:跳动气泡(3 个小圆点交错弹跳) */
.csk-launcher .csk-ico-bubble {
animation: csk-bubble-jump 1.4s ease-in-out infinite;
}
.csk-launcher .csk-ico-bubble--1 { animation-delay: 0s; }
.csk-launcher .csk-ico-bubble--2 { animation-delay: 0.18s; }
.csk-launcher .csk-ico-bubble--3 { animation-delay: 0.36s; }
@keyframes csk-bubble-jump {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-3.5px); }
60% { transform: translateY(-1px); }
}
/* 三点脉冲图标在玻璃主题下保持可见 */
/* ========== 聊天弹窗 ========== */ /* ========== 聊天弹窗 ========== */
.csk-window { .csk-window {
@ -1134,6 +1175,94 @@ function getStyles(config: ResolvedConfig): string {
box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important;
} }
/* ========== 窗口缩放拖拽手柄 ========== */
.csk-resize-handle {
position: absolute;
right: 4px;
bottom: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: flex-end;
justify-content: flex-end;
cursor: nwse-resize;
z-index: 5;
user-select: none;
-webkit-user-select: none;
/* 圆形半透明背景,与整体圆润风格一致 */
border-radius: 50%;
background: transparent;
transition: background 0.2s ease;
}
.csk-resize-handle:hover {
background: rgba(var(--csk-primary-rgb), 0.08);
}
/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */
.csk-resize-grip {
display: block;
width: 10px;
height: 10px;
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#C8CCD2 38%, #C8CCD2 42%,
transparent 42%, transparent 54%,
#C8CCD2 54%, #C8CCD2 58%,
transparent 58%, transparent 70%,
#C8CCD2 70%, #C8CCD2 74%,
transparent 74%
);
transition: background 0.2s ease;
border-radius: 2px;
}
/* hover 时线条变为主色 */
.csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary) 38%, var(--csk-primary) 42%,
transparent 42%, transparent 54%,
var(--csk-primary) 54%, var(--csk-primary) 58%,
transparent 58%, transparent 70%,
var(--csk-primary) 70%, var(--csk-primary) 74%,
transparent 74%
);
}
/* 暗色模式 */
.csk-dark .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#5B6070 38%, #5B6070 42%,
transparent 42%, transparent 54%,
#5B6070 54%, #5B6070 58%,
transparent 58%, transparent 70%,
#5B6070 70%, #5B6070 74%,
transparent 74%
);
}
.csk-dark .csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary-light) 38%, var(--csk-primary-light) 42%,
transparent 42%, transparent 54%,
var(--csk-primary-light) 54%, var(--csk-primary-light) 58%,
transparent 58%, transparent 70%,
var(--csk-primary-light) 70%, var(--csk-primary-light) 74%,
transparent 74%
);
}
/* 缩放拖拽中:禁用过渡动画 + 全局光标 */
.csk-window--resizing {
transition: none !important;
user-select: none;
}
.csk-window--resizing * { cursor: nwse-resize !important; }
/* ========== 暗色模式 ========== */ /* ========== 暗色模式 ========== */
.csk-root.csk-dark { .csk-root.csk-dark {
--csk-bg-ai: #1E1E2E; --csk-bg-ai: #1E1E2E;
@ -1401,6 +1530,7 @@ function getStyles(config: ResolvedConfig): string {
left: 0 !important; left: 0 !important;
border-radius: 0; border-radius: 0;
} }
.csk-resize-handle { display: none !important; }
.csk-window--hidden { transform: translateY(100%); } .csk-window--hidden { transform: translateY(100%); }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; } .csk-launcher:not([style*="bottom"]) { bottom: 20px; }

4
client/src/types.ts

@ -58,6 +58,8 @@ export interface SDKConfig {
showTeaser?: boolean; showTeaser?: boolean;
/** 提示气泡文字,留空则使用 i18n 默认值 */ /** 提示气泡文字,留空则使用 i18n 默认值 */
teaserText?: string; teaserText?: string;
/** 是否允许用户拖拽缩放窗口(右下角手柄),默认 true。移动端(≤480px)强制全屏时忽略 */
resizable?: boolean;
// === 行为配置 === // === 行为配置 ===
/** 是否启用流式输出,默认 true */ /** 是否启用流式输出,默认 true */
@ -132,6 +134,8 @@ export interface ResolvedConfig {
showTeaser: boolean; showTeaser: boolean;
/** 提示气泡文字 */ /** 提示气泡文字 */
teaserText: string; teaserText: string;
/** 是否允许用户拖拽缩放窗口 */
resizable: boolean;
/** 流式输出 */ /** 流式输出 */
streaming: boolean; streaming: boolean;
/** 是否启用 RAG 知识库检索 */ /** 是否启用 RAG 知识库检索 */

375
src/main/resources/static/sdk/chatbot-sdk.js

@ -96,24 +96,8 @@ var ChatbotSDK = (function () {
}, },
}; };
/** 默认悬浮按钮 SVG 图标(赛博机器人:对话气泡 + 机器人面孔 + 天线能量灯 + 眨眼动效) */
const DEFAULT_LAUNCHER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="csk-ico-grad" x1="6" y1="8" x2="26" y2="28" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#fff" stop-opacity="1"/>
<stop offset="100%" stop-color="#fff" stop-opacity="0.82"/>
</linearGradient>
</defs>
<rect x="5" y="8" width="22" height="16" rx="4" fill="url(#csk-ico-grad)"/>
<path d="M9 24L6 28.5l6-4.5z" fill="url(#csk-ico-grad)"/>
<line x1="16" y1="8" x2="16" y2="4" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="16" cy="3" r="1.5" fill="#fff" class="csk-ico-antenna"/>
<rect x="9.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<rect x="18.5" y="13" width="4" height="3.5" rx="1.2" fill="rgba(99,102,241,0.55)" class="csk-ico-eye"/>
<circle cx="11" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<circle cx="20" cy="14.2" r="0.7" fill="#fff" opacity="0.9"/>
<path d="M12.5 20 Q16 22.5 19.5 20" stroke="rgba(99,102,241,0.4)" stroke-width="1.2" fill="none" stroke-linecap="round"/>
</svg>`;
/** 默认悬浮按钮图标 — C3 三点脉冲序列:极光旋转渐变底 + 三颗顺序脉冲白点 */
const DEFAULT_LAUNCHER_ICON = `<span class="csk-ico-pulse-dot csk-ico-pulse-dot--1"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--2"></span><span class="csk-ico-pulse-dot csk-ico-pulse-dot--3"></span>`;
// ==================== 马卡龙色系玻璃质感图标 ==================== // ==================== 马卡龙色系玻璃质感图标 ====================
/** /**
* 生成玻璃质感机器人 SVG 图标 * 生成玻璃质感机器人 SVG 图标
@ -186,7 +170,7 @@ var ChatbotSDK = (function () {
* 解析并校验用户传入的配置填充默认值 * 解析并校验用户传入的配置填充默认值
*/ */
function parseConfig(raw) { function parseConfig(raw) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
// 校验必传参数:integrateId(对应后端 roleId) // 校验必传参数:integrateId(对应后端 roleId)
if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number') if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number')
|| (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) { || (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) {
@ -202,7 +186,7 @@ var ChatbotSDK = (function () {
try { try {
new URL(raw.requestDomain); new URL(raw.requestDomain);
} }
catch (_m) {
catch (_o) {
logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`);
return null; return null;
} }
@ -248,13 +232,14 @@ var ChatbotSDK = (function () {
theme: raw.theme === 'dark' ? 'dark' : 'light', theme: raw.theme === 'dark' ? 'dark' : 'light',
showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true, showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true,
teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '',
streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true,
enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : true,
resizable: (_g = raw.resizable) !== null && _g !== void 0 ? _g : true,
streaming: (_h = raw.streaming) !== null && _h !== void 0 ? _h : true,
enableRag: (_j = raw.enableRag) !== null && _j !== void 0 ? _j : true,
rewriteStrategy: raw.rewriteStrategy || 'REWRITE', rewriteStrategy: raw.rewriteStrategy || 'REWRITE',
locale: raw.locale || 'zh-CN', locale: raw.locale || 'zh-CN',
debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true,
sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false,
notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false,
debug: (_k = raw.debug) !== null && _k !== void 0 ? _k : true,
sound: (_l = raw.sound) !== null && _l !== void 0 ? _l : false,
notification: (_m = raw.notification) !== null && _m !== void 0 ? _m : false,
onError: typeof raw.onError === 'function' ? raw.onError : undefined, onError: typeof raw.onError === 'function' ? raw.onError : undefined,
onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined, onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined,
onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined,
@ -322,6 +307,7 @@ var ChatbotSDK = (function () {
// 提示气泡 // 提示气泡
teaser_text: '有什么可以帮你的吗?', teaser_text: '有什么可以帮你的吗?',
new_msg_announce: '收到新消息', new_msg_announce: '收到新消息',
resize: '拖拽缩放窗口',
// 错误提示 // 错误提示
error_network: '网络连接失败,请检查网络', error_network: '网络连接失败,请检查网络',
error_timeout: '请求超时,请稍后重试', error_timeout: '请求超时,请稍后重试',
@ -387,6 +373,7 @@ var ChatbotSDK = (function () {
// Teaser // Teaser
teaser_text: 'How can I help you?', teaser_text: 'How can I help you?',
new_msg_announce: 'New message received', new_msg_announce: 'New message received',
resize: 'Resize window',
// Errors // Errors
error_network: 'Network connection failed', error_network: 'Network connection failed',
error_timeout: 'Request timed out, please try again', error_timeout: 'Request timed out, please try again',
@ -1063,7 +1050,13 @@ var ChatbotSDK = (function () {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
background: var(--csk-bg-user);
/* 极光旋转渐变底 — 默认蓝紫系,由 primaryColor 驱动的 conic-gradient */
background: conic-gradient(from 0deg,
var(--csk-primary-light, #06b6d4),
var(--csk-primary, #3b82f6),
var(--csk-primary-hover, #8b5cf6),
var(--csk-primary-light, #06b6d4)
);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1083,7 +1076,9 @@ var ChatbotSDK = (function () {
transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.25s ease, box-shadow 0.25s ease,
border-color 0.25s ease; border-color 0.25s ease;
animation: csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation:
csk-aurora-spin 4s linear infinite,
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.csk-launcher--right { right: 24px; } .csk-launcher--right { right: 24px; }
.csk-launcher--left { left: 24px; } .csk-launcher--left { left: 24px; }
@ -1147,6 +1142,64 @@ var ChatbotSDK = (function () {
60% { transform: scale(1.06) translateY(-2px); } 60% { transform: scale(1.06) translateY(-2px); }
100% { opacity: 1; transform: scale(1) translateY(0); } 100% { opacity: 1; transform: scale(1) translateY(0); }
} }
/* ========== 三点脉冲序列图标 ========== */
/* 极光旋转 */
@keyframes csk-aurora-spin {
to { transform: rotate(360deg); }
}
/* 脉冲点 */
.csk-ico-pulse-dot {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background: #fff;
z-index: 5;
animation: csk-seq-pulse 1.2s ease-in-out infinite;
box-shadow: 0 0 6px 1px rgba(255, 255, 255, 0.5);
}
.csk-ico-pulse-dot--1 { top: 4px; left: 50%; margin-left: -3px; }
.csk-ico-pulse-dot--2 { top: 50%; right: 5px; margin-top: -3px; animation-delay: 0.3s; }
.csk-ico-pulse-dot--3 { bottom: 5px; left: 50%; margin-left: -3px; animation-delay: 0.6s; }
@keyframes csk-seq-pulse {
0%, 100% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
35% { transform: scale(1.8); opacity: 1; box-shadow: 0 0 12px 4px rgba(255, 255, 255, 0.8); }
70% { transform: scale(0.4); opacity: 0.3; box-shadow: 0 0 4px 1px rgba(255, 255, 255, 0.3); }
}
/* 移除旧的 SVG 图标相关样式(已不再使用) */
/* ========== 高光扫过动效 ========== */
.csk-launcher::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 40%;
height: 200%;
border-radius: 50%;
background: linear-gradient(
105deg,
transparent 30%,
rgba(255, 255, 255, 0.28) 48%,
rgba(255, 255, 255, 0.1) 52%,
transparent 70%
);
transform: rotate(25deg);
animation: csk-shimmer 4s ease-in-out infinite;
pointer-events: none;
}
/* 呼吸色晕 — 配合接地阴影,强化白底上的立体存在感 */
.csk-launcher::after {
content: '';
position: absolute;
inset: -18px;
border-radius: 50%;
background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.18) 0%, rgba(var(--csk-primary-rgb), 0.05) 45%, transparent 68%);
animation: csk-breathe 3.5s ease-in-out infinite;
pointer-events: none;
}
@keyframes csk-shimmer { @keyframes csk-shimmer {
0%, 100% { left: -60%; opacity: 0; } 0%, 100% { left: -60%; opacity: 0; }
20% { opacity: 1; } 20% { opacity: 1; }
@ -1158,25 +1211,11 @@ var ChatbotSDK = (function () {
50% { transform: scale(1.15); opacity: 0.35; } 50% { transform: scale(1.15); opacity: 0.35; }
} }
/* SVG 图标内部动效:眨眼 */
.csk-launcher .csk-ico-eye {
animation: csk-eye-blink 4s ease-in-out infinite;
}
@keyframes csk-eye-blink {
0%, 42%, 48%, 100% { opacity: 1; }
45% { opacity: 0.1; }
}
/* SVG 图标内部动效:天线能量闪烁 */
.csk-launcher .csk-ico-antenna {
animation: csk-antenna-glow 2s ease-in-out infinite;
}
@keyframes csk-antenna-glow {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* ========== 磨砂玻璃质感悬浮按钮 ========== */ /* ========== 磨砂玻璃质感悬浮按钮 ========== */
.csk-launcher--glass { .csk-launcher--glass {
/* 覆盖旋转动画为静止渐变 */
animation:
csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both !important;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--csk-launcher-grad-1, var(--csk-primary)) 0%, var(--csk-launcher-grad-1, var(--csk-primary)) 0%,
@ -1237,18 +1276,7 @@ var ChatbotSDK = (function () {
70% { left: 130%; opacity: 0; } 70% { left: 130%; opacity: 0; }
} }
/* SVG 内部动效:跳动气泡(3 个小圆点交错弹跳) */
.csk-launcher .csk-ico-bubble {
animation: csk-bubble-jump 1.4s ease-in-out infinite;
}
.csk-launcher .csk-ico-bubble--1 { animation-delay: 0s; }
.csk-launcher .csk-ico-bubble--2 { animation-delay: 0.18s; }
.csk-launcher .csk-ico-bubble--3 { animation-delay: 0.36s; }
@keyframes csk-bubble-jump {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-3.5px); }
60% { transform: translateY(-1px); }
}
/* 三点脉冲图标在玻璃主题下保持可见 */
/* ========== 聊天弹窗 ========== */ /* ========== 聊天弹窗 ========== */
.csk-window { .csk-window {
@ -2105,6 +2133,94 @@ var ChatbotSDK = (function () {
box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important;
} }
/* ========== 窗口缩放拖拽手柄 ========== */
.csk-resize-handle {
position: absolute;
right: 4px;
bottom: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: flex-end;
justify-content: flex-end;
cursor: nwse-resize;
z-index: 5;
user-select: none;
-webkit-user-select: none;
/* 圆形半透明背景,与整体圆润风格一致 */
border-radius: 50%;
background: transparent;
transition: background 0.2s ease;
}
.csk-resize-handle:hover {
background: rgba(var(--csk-primary-rgb), 0.08);
}
/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */
.csk-resize-grip {
display: block;
width: 10px;
height: 10px;
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#C8CCD2 38%, #C8CCD2 42%,
transparent 42%, transparent 54%,
#C8CCD2 54%, #C8CCD2 58%,
transparent 58%, transparent 70%,
#C8CCD2 70%, #C8CCD2 74%,
transparent 74%
);
transition: background 0.2s ease;
border-radius: 2px;
}
/* hover 时线条变为主色 */
.csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary) 38%, var(--csk-primary) 42%,
transparent 42%, transparent 54%,
var(--csk-primary) 54%, var(--csk-primary) 58%,
transparent 58%, transparent 70%,
var(--csk-primary) 70%, var(--csk-primary) 74%,
transparent 74%
);
}
/* 暗色模式 */
.csk-dark .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
#5B6070 38%, #5B6070 42%,
transparent 42%, transparent 54%,
#5B6070 54%, #5B6070 58%,
transparent 58%, transparent 70%,
#5B6070 70%, #5B6070 74%,
transparent 74%
);
}
.csk-dark .csk-resize-handle:hover .csk-resize-grip {
background:
linear-gradient(135deg,
transparent 0%, transparent 38%,
var(--csk-primary-light) 38%, var(--csk-primary-light) 42%,
transparent 42%, transparent 54%,
var(--csk-primary-light) 54%, var(--csk-primary-light) 58%,
transparent 58%, transparent 70%,
var(--csk-primary-light) 70%, var(--csk-primary-light) 74%,
transparent 74%
);
}
/* 缩放拖拽中:禁用过渡动画 + 全局光标 */
.csk-window--resizing {
transition: none !important;
user-select: none;
}
.csk-window--resizing * { cursor: nwse-resize !important; }
/* ========== 暗色模式 ========== */ /* ========== 暗色模式 ========== */
.csk-root.csk-dark { .csk-root.csk-dark {
--csk-bg-ai: #1E1E2E; --csk-bg-ai: #1E1E2E;
@ -2372,6 +2488,7 @@ var ChatbotSDK = (function () {
left: 0 !important; left: 0 !important;
border-radius: 0; border-radius: 0;
} }
.csk-resize-handle { display: none !important; }
.csk-window--hidden { transform: translateY(100%); } .csk-window--hidden { transform: translateY(100%); }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; } .csk-launcher:not([style*="bottom"]) { bottom: 20px; }
@ -2719,6 +2836,13 @@ var ChatbotSDK = (function () {
<button class="csk-teaser__close" aria-label="${t('close')}" type="button">&times;</button> <button class="csk-teaser__close" aria-label="${t('close')}" type="button">&times;</button>
<span>${config.teaserText || t('teaser_text')}</span> <span>${config.teaserText || t('teaser_text')}</span>
`; `;
// === 缩放拖拽手柄(右下角) ===
const resizeHandle = document.createElement('div');
resizeHandle.className = 'csk-resize-handle';
resizeHandle.setAttribute('aria-label', t('resize'));
// 纯 CSS 渲染三条斜线,无需 SVG
resizeHandle.innerHTML = '<span class="csk-resize-grip"></span>';
windowEl.appendChild(resizeHandle);
return { return {
window: windowEl, window: windowEl,
messagesContainer, messagesContainer,
@ -2730,6 +2854,7 @@ var ChatbotSDK = (function () {
welcomeEl, welcomeEl,
newMsgBtn, newMsgBtn,
teaserEl, teaserEl,
resizeHandle,
searchInput, searchInput,
ariaLiveEl, ariaLiveEl,
showLoading, showLoading,
@ -2939,6 +3064,100 @@ var ChatbotSDK = (function () {
document.removeEventListener('touchend', onTouchEnd); document.removeEventListener('touchend', onTouchEnd);
}; };
} }
// ==================== 窗口缩放支持 ====================
/** 窗口最小尺寸 */
const MIN_WIDTH = 300;
const MIN_HEIGHT = 300;
/**
* 启用窗口右下角拖拽缩放
* @param handleEl 缩放手柄元素
* @param windowEl 聊天窗口元素
* @param onResizeEnd 缩放结束回调返回最终宽高用于持久化
* @returns 清理函数
*/
function enableResize(handleEl, windowEl, onResizeEnd) {
let resizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
const onPointerDown = (clientX, clientY) => {
resizing = true;
startX = clientX;
startY = clientY;
const rect = windowEl.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
windowEl.classList.add('csk-window--resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
};
const onPointerMove = (clientX, clientY, e) => {
if (!resizing)
return;
if (e)
e.preventDefault();
const dx = clientX - startX;
const dy = clientY - startY;
const newWidth = Math.max(MIN_WIDTH, Math.min(startWidth + dx, window.innerWidth - 24));
const newHeight = Math.max(MIN_HEIGHT, Math.min(startHeight + dy, window.innerHeight - 24));
windowEl.style.width = `${newWidth}px`;
windowEl.style.height = `${newHeight}px`;
// 同步更新 CSS 变量(供内部布局使用)
windowEl.style.setProperty('--csk-window-width', `${newWidth}px`);
windowEl.style.setProperty('--csk-window-height', `${newHeight}px`);
// 拖拽缩放后清除 bottom/right 定位,避免与固定定位冲突
windowEl.style.bottom = '';
windowEl.style.right = '';
};
const onPointerUp = () => {
if (!resizing)
return;
resizing = false;
windowEl.classList.remove('csk-window--resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
const rect = windowEl.getBoundingClientRect();
if (onResizeEnd) {
onResizeEnd({ width: rect.width, height: rect.height });
}
};
// 鼠标事件(DOM 事件监听器包装)
function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp() { onPointerUp(); }
// 鼠标事件
function onMouseDown(e) {
e.preventDefault();
e.stopPropagation();
onPointerDown(e.clientX, e.clientY);
}
// 触摸事件
function onTouchStart(e) {
e.preventDefault();
e.stopPropagation();
if (e.touches.length === 1)
onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e) {
if (e.touches.length === 1)
onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd() { onPointerUp(); }
handleEl.addEventListener('mousedown', onMouseDown);
handleEl.addEventListener('touchstart', onTouchStart, { passive: false });
return () => {
handleEl.removeEventListener('mousedown', onMouseDown);
handleEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ==================== // ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */ /** 渲染用户消息气泡 */
function renderUserBubble(container, text, timestamp) { function renderUserBubble(container, text, timestamp) {
@ -4388,6 +4607,7 @@ var ChatbotSDK = (function () {
let hideLoadingFn = null; let hideLoadingFn = null;
let dragCleanup = null; let dragCleanup = null;
let launcherDragCleanup = null; let launcherDragCleanup = null;
let resizeCleanup = null;
/** Launcher 位置记忆状态(拖拽后更新) */ /** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos = null; let launcherPos = null;
/** 提示气泡元素 */ /** 提示气泡元素 */
@ -4468,6 +4688,15 @@ var ChatbotSDK = (function () {
} }
// 9.1 恢复上次拖拽位置 // 9.1 恢复上次拖拽位置
restoreWindowPosition(config.integrateId, windowEl); restoreWindowPosition(config.integrateId, windowEl);
// 9.2 启用缩放(resizable 配置控制)
if (config.resizable) {
const resizeHandle = dom.resizeHandle;
resizeCleanup = enableResize(resizeHandle, windowEl, (size) => {
saveWindowSize(config.integrateId, size);
});
// 恢复上次缩放的尺寸
restoreWindowSize(config.integrateId, windowEl);
}
// 10. 初始化对话模块 // 10. 初始化对话模块
initChat(config, { initChat(config, {
messagesContainer, messagesContainer,
@ -4686,6 +4915,10 @@ var ChatbotSDK = (function () {
launcherDragCleanup(); launcherDragCleanup();
launcherDragCleanup = null; launcherDragCleanup = null;
} }
if (resizeCleanup) {
resizeCleanup();
resizeCleanup = null;
}
launcherPos = null; launcherPos = null;
removeStyles(); removeStyles();
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDown);
@ -4741,6 +4974,36 @@ var ChatbotSDK = (function () {
clearMessages(config.integrateId); clearMessages(config.integrateId);
} }
} }
// ==================== 窗口尺寸记忆 ====================
/** localStorage key:窗口尺寸 */
function sizeKey(integrateId) {
return `csk_size_${integrateId}`;
}
/** 保存窗口缩放的尺寸到 localStorage */
function saveWindowSize(integrateId, size) {
try {
localStorage.setItem(sizeKey(integrateId), JSON.stringify(size));
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 恢复窗口缩放尺寸 */
function restoreWindowSize(integrateId, winEl) {
try {
const raw = localStorage.getItem(sizeKey(integrateId));
if (!raw)
return;
const size = JSON.parse(raw);
if (typeof size.width !== 'number' || typeof size.height !== 'number')
return;
const w = Math.max(300, Math.min(size.width, window.innerWidth - 24));
const h = Math.max(300, Math.min(size.height, window.innerHeight - 24));
winEl.style.width = `${w}px`;
winEl.style.height = `${h}px`;
winEl.style.setProperty('--csk-window-width', `${w}px`);
winEl.style.setProperty('--csk-window-height', `${h}px`);
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
// ==================== 窗口位置记忆 ==================== // ==================== 窗口位置记忆 ====================
/** localStorage key:窗口位置 */ /** localStorage key:窗口位置 */
function positionKey(integrateId) { function positionKey(integrateId) {

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

56
src/main/resources/static/sdk/test.html

@ -166,12 +166,32 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
<div class="fg-row"> <div class="fg-row">
<div class="fg"> <div class="fg">
<label>width (px)</label> <label>width (px)</label>
<input type="number" id="cfg-width" value="380" min="300" max="600">
<input type="number" id="cfg-width" value="380" min="300" max="1200">
</div> </div>
<div class="fg">
<label>height (px)</label>
<input type="number" id="cfg-height" value="520" min="300" max="1200">
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>resizable 可缩放</label>
<select id="cfg-resizable"><option value="1" selected>开启</option><option value="0">关闭</option></select>
</div>
<div class="fg">
<label>theme 主题模式</label>
<select id="cfg-theme-mode"><option value="light" selected>白天 light</option><option value="dark">暗夜 dark</option></select>
</div>
</div>
<div class="fg-row">
<div class="fg"> <div class="fg">
<label>streaming</label> <label>streaming</label>
<select id="cfg-stream"><option value="1" selected>开启流式</option><option value="0">关闭(同步)</option></select> <select id="cfg-stream"><option value="1" selected>开启流式</option><option value="0">关闭(同步)</option></select>
</div> </div>
<div class="fg">
<label>enableRag 知识库检索</label>
<select id=cfg-rag><option value=1 selected>开启</option><option value=0>关闭</option></select>
</div>
</div> </div>
<div class="fg-row"> <div class="fg-row">
<div class="fg"> <div class="fg">
@ -179,25 +199,25 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
<select id="cfg-locale"><option value="zh-CN" selected>中文</option><option value="en">English</option></select> <select id="cfg-locale"><option value="zh-CN" selected>中文</option><option value="en">English</option></select>
</div> </div>
<div class="fg"> <div class="fg">
<label>enableRag 知识库检索</label>
<select id=cfg-rag><option value=1 selected>开启</option><option value=0>关闭</option></select>
<label>debug</label>
<select id="cfg-debug"><option value="1" selected>开启日志</option><option value="0">关闭日志</option></select>
</div> </div>
</div> </div>
<div class="fg-row"> <div class="fg-row">
<div class="fg"> <div class="fg">
<label>launcherTheme 悬浮按钮主题</label> <label>launcherTheme 悬浮按钮主题</label>
<select id="cfg-theme"> <select id="cfg-theme">
<option value="" selected>默认(primaryColor 渐变</option>
<option value="" selected>默认(三点脉冲旋转极光</option>
<option value="dream-purple">🔮 梦幻紫粉</option> <option value="dream-purple">🔮 梦幻紫粉</option>
<option value="mint-tech">🌿 薄荷青绿</option> <option value="mint-tech">🌿 薄荷青绿</option>
<option value="coral-peach">🍑 珊瑚蜜桃</option> <option value="coral-peach">🍑 珊瑚蜜桃</option>
<option value="sky-blue">🌊 天空蓝紫</option> <option value="sky-blue">🌊 天空蓝紫</option>
</select> </select>
<div class="hint">磨砂玻璃 + 呼吸发光 + 跳动气泡</div>
<div class="hint">磨砂玻璃 + 呼吸发光</div>
</div> </div>
<div class="fg"> <div class="fg">
<label>debug</label>
<select id="cfg-debug"><option value="1" selected>开启日志</option><option value="0">关闭日志</option></select>
<label>showClear 清空按钮</label>
<select id="cfg-clear"><option value="1" selected>显示</option><option value="0">隐藏</option></select>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
@ -220,9 +240,10 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
1. integrateId 填写客服角色 ID(数字,对应后端 roleId)<br> 1. integrateId 填写客服角色 ID(数字,对应后端 roleId)<br>
2. userId 填写外部用户ID(对应后端 accountId)<br> 2. userId 填写外部用户ID(对应后端 accountId)<br>
3. chatId 自动管理:从 /conversation/list 获取或自动生成<br> 3. chatId 自动管理:从 /conversation/list 获取或自动生成<br>
4. 对话历史从后端加载(点击头部时钟图标查看)<br>
5. AI 回复支持 Markdown 渲染<br>
6. SDK 默认启用 RAG 检索;开启 showCategorySwitch 可选择知识库分类
4. 拖拽窗口右下角手柄可缩放窗口,尺寸自动保存<br>
5. 对话历史从后端加载(点击头部时钟图标查看)<br>
6. AI 回复支持 Markdown 渲染<br>
7. SDK 默认启用 RAG 检索;开启 showCategorySwitch 可选择知识库分类
</div> </div>
<hr class="divider"> <hr class="divider">
@ -324,12 +345,15 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
primaryColor: getEl('cfg-color').value, primaryColor: getEl('cfg-color').value,
position: getEl('cfg-pos').value, position: getEl('cfg-pos').value,
width: parseInt(getEl('cfg-width').value) || 380, width: parseInt(getEl('cfg-width').value) || 380,
height: parseInt(getEl('cfg-height').value) || 520,
resizable: getEl('cfg-resizable').value === '1',
streaming: getEl('cfg-stream').value === '1', streaming: getEl('cfg-stream').value === '1',
locale: getEl('cfg-locale').value, locale: getEl('cfg-locale').value,
showCategorySwitch: getEl('cfg-cat').value === '1', showCategorySwitch: getEl('cfg-cat').value === '1',
enableRag: getEl('cfg-rag').value === '1', enableRag: getEl('cfg-rag').value === '1',
debug: getEl('cfg-debug').value === '1', debug: getEl('cfg-debug').value === '1',
showClear: true,
theme: getEl('cfg-theme-mode').value,
showClear: getEl('cfg-clear').value === '1',
launcherTheme: getEl('cfg-theme').value || undefined, launcherTheme: getEl('cfg-theme').value || undefined,
}; };
// 有 Token 时传入鉴权参数 // 有 Token 时传入鉴权参数
@ -430,12 +454,16 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
const SDK_DEFAULTS = { const SDK_DEFAULTS = {
title: 'AI 智能助手', title: 'AI 智能助手',
width: 380, width: 380,
height: 520,
position: 'right-bottom', position: 'right-bottom',
primaryColor: '#4F46E5', primaryColor: '#4F46E5',
streaming: true, streaming: true,
locale: 'zh-CN', locale: 'zh-CN',
showCategorySwitch: false, showCategorySwitch: false,
enableRag: true, enableRag: true,
resizable: true,
theme: 'light',
showClear: true,
}; };
// 生成 Script 片段代码(纯文本) // 生成 Script 片段代码(纯文本)
@ -465,10 +493,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
if (cfg.primaryColor !== SDK_DEFAULTS.primaryColor) params.push(' primaryColor: ' + JSON.stringify(cfg.primaryColor) + ', // 主色调'); if (cfg.primaryColor !== SDK_DEFAULTS.primaryColor) params.push(' primaryColor: ' + JSON.stringify(cfg.primaryColor) + ', // 主色调');
if (cfg.position !== SDK_DEFAULTS.position) params.push(' position: ' + JSON.stringify(cfg.position) + ', // 悬浮按钮位置'); if (cfg.position !== SDK_DEFAULTS.position) params.push(' position: ' + JSON.stringify(cfg.position) + ', // 悬浮按钮位置');
if (cfg.width !== SDK_DEFAULTS.width) params.push(' width: ' + cfg.width + ', // 弹窗宽度(px)'); if (cfg.width !== SDK_DEFAULTS.width) params.push(' width: ' + cfg.width + ', // 弹窗宽度(px)');
if (cfg.height !== SDK_DEFAULTS.height) params.push(' height: ' + cfg.height + ', // 弹窗高度(px),最小 300');
if (cfg.resizable !== SDK_DEFAULTS.resizable) params.push(' resizable: ' + cfg.resizable + ', // 允许拖拽缩放窗口');
if (cfg.theme !== SDK_DEFAULTS.theme) params.push(' theme: ' + JSON.stringify(cfg.theme) + ', // 主题模式');
if (cfg.streaming !== SDK_DEFAULTS.streaming) params.push(' streaming: ' + cfg.streaming + ', // 是否启用流式输出'); if (cfg.streaming !== SDK_DEFAULTS.streaming) params.push(' streaming: ' + cfg.streaming + ', // 是否启用流式输出');
if (cfg.locale !== SDK_DEFAULTS.locale) params.push(' locale: ' + JSON.stringify(cfg.locale) + ', // 界面语言'); if (cfg.locale !== SDK_DEFAULTS.locale) params.push(' locale: ' + JSON.stringify(cfg.locale) + ', // 界面语言');
if (cfg.showCategorySwitch !== SDK_DEFAULTS.showCategorySwitch) params.push(' showCategorySwitch: ' + cfg.showCategorySwitch + ', // 是否显示知识库切换'); if (cfg.showCategorySwitch !== SDK_DEFAULTS.showCategorySwitch) params.push(' showCategorySwitch: ' + cfg.showCategorySwitch + ', // 是否显示知识库切换');
if (cfg.enableRag !== SDK_DEFAULTS.enableRag) params.push(' enableRag: ' + cfg.enableRag + ', // 启用 RAG 知识库检索'); if (cfg.enableRag !== SDK_DEFAULTS.enableRag) params.push(' enableRag: ' + cfg.enableRag + ', // 启用 RAG 知识库检索');
if (cfg.showClear !== SDK_DEFAULTS.showClear) params.push(' showClear: ' + cfg.showClear + ', // 显示清空按钮');
if (cfg.launcherTheme) params.push(' launcherTheme: ' + JSON.stringify(cfg.launcherTheme) + ', // 悬浮按钮主题(磨砂玻璃质感)'); if (cfg.launcherTheme) params.push(' launcherTheme: ' + JSON.stringify(cfg.launcherTheme) + ', // 悬浮按钮主题(磨砂玻璃质感)');
// debug 强制 false(生产环境推荐),无论面板当前值 // debug 强制 false(生产环境推荐),无论面板当前值
params.push(' debug: false // 调试日志,生产环境建议关闭'); params.push(' debug: false // 调试日志,生产环境建议关闭');
@ -660,7 +692,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
setEl('footer-time',new Date().toLocaleTimeString()); setEl('footer-time',new Date().toLocaleTimeString());
if(typeof window.ChatbotSDK!=='undefined'){const tag=getEl('tag-sdk');tag.className='tag tag--pass';tag.textContent='✅ SDK 已加载';} if(typeof window.ChatbotSDK!=='undefined'){const tag=getEl('tag-sdk');tag.className='tag tag--pass';tag.textContent='✅ SDK 已加载';}
// 监听配置表单变化,实时刷新接入代码 // 监听配置表单变化,实时刷新接入代码
['cfg-iid','cfg-domain','cfg-uid','cfg-title','cfg-color','cfg-pos','cfg-width','cfg-stream','cfg-locale','cfg-cat','cfg-debug','cfg-theme','cfg-apikey','cfg-token'].forEach(function(id){
['cfg-iid','cfg-domain','cfg-uid','cfg-title','cfg-color','cfg-pos','cfg-width','cfg-height','cfg-resizable','cfg-stream','cfg-locale','cfg-cat','cfg-rag','cfg-clear','cfg-debug','cfg-theme','cfg-theme-mode','cfg-apikey','cfg-token'].forEach(function(id){
const el=getEl(id); const el=getEl(id);
if(!el)return; if(!el)return;
el.addEventListener('input', generateCode); el.addEventListener('input', generateCode);

Loading…
Cancel
Save