diff --git a/frontend/public/assistant.js b/frontend/public/assistant.js new file mode 100644 index 0000000..382d995 --- /dev/null +++ b/frontend/public/assistant.js @@ -0,0 +1,748 @@ +;(function () { + window.sqlbot_assistant_handler = window.sqlbot_assistant_handler || {} + const defaultData = { + id: '1', + show_guide: false, + float_icon: '', + domain_url: 'http://localhost:5173', + header_font_color: 'rgb(100, 106, 115)', + x_type: 'right', + y_type: 'bottom', + x_val: '30', + y_val: '30', + float_icon_drag: false, + } + const script_id_prefix = 'sqlbot-assistant-float-script-' + const guideHtml = ` +
+
+
+
+
+ + + +
+ +
🌟 遇见问题,不再有障碍!
+

你好,我是你的智能小助手。
+ 点我,开启高效解答模式,让问题变成过去式。

+
+ +
+ +
+` + + const chatButtonHtml = (data) => ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
` + + const getChatContainerHtml = (data) => { + return ` +
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+` + } + /** + * 初始化引导 + * @param {*} root + */ + const initGuide = (root) => { + root.insertAdjacentHTML('beforeend', guideHtml) + const button = root.querySelector('.sqlbot-assistant-button') + const close_icon = root.querySelector('.sqlbot-assistant-close') + const close_func = () => { + root.removeChild(root.querySelector('.sqlbot-assistant-tips')) + root.removeChild(root.querySelector('.sqlbot-assistant-mask')) + localStorage.setItem('sqlbot_assistant_mask_tip', true) + } + button.onclick = close_func + close_icon.onclick = close_func + } + const initChat = (root, data) => { + // 添加对话icon + root.insertAdjacentHTML('beforeend', chatButtonHtml(data)) + // 添加对话框 + root.insertAdjacentHTML('beforeend', getChatContainerHtml(data)) + // 按钮元素 + const chat_button = root.querySelector('.sqlbot-assistant-chat-button') + let chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > svg') + if (data.float_icon) { + chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img') + } + chat_button_img.style.display = 'block' + // 对话框元素 + const chat_container = root.querySelector('#sqlbot-assistant-chat-container') + // 引导层 + const mask_content = root.querySelector('.sqlbot-assistant-mask > .sqlbot-assistant-content') + const mask_tips = root.querySelector('.sqlbot-assistant-tips') + chat_button_img.onload = (event) => { + if (mask_content) { + mask_content.style.width = chat_button_img.width + 'px' + mask_content.style.height = chat_button_img.height + 'px' + if (data.x_type == 'left') { + mask_tips.style.marginLeft = + (chat_button_img.naturalWidth > 500 ? 500 : chat_button_img.naturalWidth) - 64 + 'px' + } else { + mask_tips.style.marginRight = + (chat_button_img.naturalWidth > 500 ? 500 : chat_button_img.naturalWidth) - 64 + 'px' + } + } + } + + const viewport = root.querySelector('.sqlbot-assistant-openviewport') + const closeviewport = root.querySelector('.sqlbot-assistant-closeviewport') + const close_func = () => { + chat_container.style['display'] = + chat_container.style['display'] == 'block' ? 'none' : 'block' + chat_button.style['display'] = chat_container.style['display'] == 'block' ? 'none' : 'block' + } + close_icon = chat_container.querySelector('.sqlbot-assistant-chat-close') + chat_button.onclick = close_func + close_icon.onclick = close_func + const viewport_func = () => { + if (chat_container.classList.contains('sqlbot-assistant-enlarge')) { + chat_container.classList.remove('sqlbot-assistant-enlarge') + viewport.classList.remove('sqlbot-assistant-viewportnone') + closeviewport.classList.add('sqlbot-assistant-viewportnone') + } else { + chat_container.classList.add('sqlbot-assistant-enlarge') + viewport.classList.add('sqlbot-assistant-viewportnone') + closeviewport.classList.remove('sqlbot-assistant-viewportnone') + } + } + if (data.float_icon_drag) { + chat_button.setAttribute('draggable', 'true') + + let startX = 0 + let startY = 0 + const img = new Image() + img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=' + chat_button.addEventListener('dragstart', (e) => { + startX = e.clientX - chat_button.offsetLeft + startY = e.clientY - chat_button.offsetTop + e.dataTransfer.setDragImage(img, 0, 0) + }) + + chat_button.addEventListener('drag', (e) => { + if (e.clientX && e.clientY) { + const left = e.clientX - startX + const top = e.clientY - startY + + const maxX = window.innerWidth - chat_button.offsetWidth + const maxY = window.innerHeight - chat_button.offsetHeight + + chat_button.style.left = Math.min(Math.max(0, left), maxX) + 'px' + chat_button.style.top = Math.min(Math.max(0, top), maxY) + 'px' + } + }) + + let touchStartX = 0 + let touchStartY = 0 + + chat_button.addEventListener('touchstart', (e) => { + touchStartX = e.touches[0].clientX - chat_button.offsetLeft + touchStartY = e.touches[0].clientY - chat_button.offsetTop + e.preventDefault() + }) + + chat_button.addEventListener('touchmove', (e) => { + const left = e.touches[0].clientX - touchStartX + const top = e.touches[0].clientY - touchStartY + + const maxX = window.innerWidth - chat_button.offsetWidth + const maxY = window.innerHeight - chat_button.offsetHeight + + chat_button.style.left = Math.min(Math.max(0, left), maxX) + 'px' + chat_button.style.top = Math.min(Math.max(0, top), maxY) + 'px' + + e.preventDefault() + }) + } + /* const drag = (e) => { + if (['touchmove', 'touchstart'].includes(e.type)) { + chat_button.style.top = e.touches[0].clientY - chat_button_img.clientHeight / 2 + 'px' + chat_button.style.left = e.touches[0].clientX - chat_button_img.clientHeight / 2 + 'px' + } else { + chat_button.style.top = e.y - chat_button_img.clientHeight / 2 + 'px' + chat_button.style.left = e.x - chat_button_img.clientHeight / 2 + 'px' + } + chat_button.style.width = chat_button_img.clientHeight + 'px' + chat_button.style.height = chat_button_img.clientHeight + 'px' + } + if (data.float_icon_drag) { + chat_button.setAttribute('draggable', 'true') + chat_button.addEventListener('drag', drag) + chat_button.addEventListener('dragover', (e) => { + e.preventDefault() + }) + chat_button.addEventListener('dragend', drag) + chat_button.addEventListener('touchstart', drag) + chat_button.addEventListener('touchmove', drag) + } */ + viewport.onclick = viewport_func + closeviewport.onclick = viewport_func + } + /** + * 第一次进来的引导提示 + */ + function initsqlbot_assistant(data) { + const sqlbot_div = document.createElement('div') + const root = document.createElement('div') + const sqlbot_root_id = 'sqlbot-assistant-root-' + data.id + root.id = sqlbot_root_id + initsqlbot_assistantStyle(sqlbot_div, sqlbot_root_id, data) + sqlbot_div.appendChild(root) + document.body.appendChild(sqlbot_div) + const sqlbot_assistant_mask_tip = localStorage.getItem('sqlbot_assistant_mask_tip') + if (sqlbot_assistant_mask_tip == null && data.show_guide) { + initGuide(root) + } + initChat(root, data) + } + + // 初始化全局样式 + function initsqlbot_assistantStyle(root, sqlbot_assistantId, data) { + style = document.createElement('style') + style.type = 'text/css' + style.innerText = ` + /* 放大 */ + #sqlbot-assistant .sqlbot-assistant-enlarge { + width: 50%!important; + height: 100%!important; + bottom: 0!important; + right: 0 !important; + } + @media only screen and (max-width: 768px){ + #sqlbot-assistant .sqlbot-assistant-enlarge { + width: 100%!important; + height: 100%!important; + right: 0 !important; + bottom: 0!important; + } + } + + /* 引导 */ + + #sqlbot-assistant .sqlbot-assistant-mask { + position: fixed; + z-index: 10001; + background-color: transparent; + height: 100%; + width: 100%; + top: 0; + left: 0; + } + #sqlbot-assistant .sqlbot-assistant-mask .sqlbot-assistant-content { + width: 64px; + height: 64px; + box-shadow: 1px 1px 1px 9999px rgba(0,0,0,.6); + position: absolute; + ${data.x_type}: ${data.x_val}px; + ${data.y_type}: ${data.y_val}px; + z-index: 10001; + } + #sqlbot-assistant .sqlbot-assistant-tips { + position: fixed; + ${data.x_type}:calc(${data.x_val}px + 75px); + ${data.y_type}: calc(${data.y_val}px + 0px); + padding: 22px 24px 24px; + border-radius: 6px; + color: #ffffff; + font-size: 14px; + background: #3370FF; + z-index: 10001; + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-arrow { + position: absolute; + background: #3370FF; + width: 10px; + height: 10px; + pointer-events: none; + transform: rotate(45deg); + box-sizing: border-box; + /* left */ + ${data.x_type}: -5px; + ${data.y_type}: 33px; + border-left-color: transparent; + border-bottom-color: transparent + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-title { + font-size: 20px; + font-weight: 500; + margin-bottom: 8px; + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button { + text-align: right; + margin-top: 24px; + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button button { + border-radius: 4px; + background: #FFF; + padding: 3px 12px; + color: #3370FF; + cursor: pointer; + outline: none; + border: none; + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button button::after{ + border: none; + } + #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-close { + position: absolute; + right: 20px; + top: 20px; + cursor: pointer; + + } + #sqlbot-assistant-chat-container { + width: 460px; + height: 640px; + display:none; + } + @media only screen and (max-width: 768px) { + #sqlbot-assistant-chat-container { + width: 100%; + height: 70%; + right: 0 !important; + } + } + + #sqlbot-assistant .sqlbot-assistant-chat-button{ + position: fixed; + ${data.x_type}: ${data.x_val}px; + ${data.y_type}: ${data.y_val}px; + cursor: pointer; + z-index:10000; + } + #sqlbot-assistant #sqlbot-assistant-chat-container{ + z-index:10000;position: relative; + border-radius: 8px; + //border: 1px solid #ffffff; + background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1; + box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10); + position: fixed;bottom: 16px;right: 16px;overflow: hidden; + } + + .ed-overlay-dialog { + margin-top: 50px; + } + .ed-drawer { + margin-top: 50px; + } + + #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate{ + top: 18px; + right: 15px; + position: absolute; + display: flex; + align-items: center; + line-height: 18px; + } + #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-chat-close{ + margin-left:15px; + cursor: pointer; + } + #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-openviewport{ + + cursor: pointer; + } + #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-closeviewport{ + + cursor: pointer; + } + #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-viewportnone{ + display:none; + } + #sqlbot-assistant #sqlbot-assistant-chat-container #sqlbot-assistant-chat-iframe-${data.id} { + height:100%; + width:100%; + border: none; + } + #sqlbot-assistant #sqlbot-assistant-chat-container { + animation: appear .4s ease-in-out; + } + @keyframes appear { + from { + height: 0;; + } + + to { + height: 600px; + } + }`.replaceAll('#sqlbot-assistant ', `#${sqlbot_assistantId} `) + root.appendChild(style) + } + function getParam(src, key) { + const url = new URL(src) + return url.searchParams.get(key) + } + function parsrCertificate(config) { + const certificateList = config.certificate + if (!certificateList?.length) { + return null + } + const list = certificateList.map((item) => formatCertificate(item)).filter((item) => !!item) + return JSON.stringify(list) + } + function isEmpty(obj) { + return obj == null || typeof obj == 'undefined' + } + function formatCertificate(item) { + const { type, source, target, target_key, target_val } = item + let source_val = null + if (type.toLocaleLowerCase() == 'localstorage') { + source_val = localStorage.getItem(source) + } + if (type.toLocaleLowerCase() == 'sessionstorage') { + source_val = sessionStorage.getItem(source) + } + if (type.toLocaleLowerCase() == 'cookie') { + source_val = getCookie(source) + } + if (type.toLocaleLowerCase() == 'custom') { + source_val = source + } + if (isEmpty(source_val)) { + return null + } + return { + target, + key: target_key || source, + value: (target_val && eval(target_val)) || source_val, + } + } + function getCookie(key) { + if (!key || !document.cookie) { + return null + } + const cookies = document.cookie.split(';') + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim() + + if (cookie.startsWith(key + '=')) { + return decodeURIComponent(cookie.substring(key.length + 1)) + } + } + return null + } + function registerMessageEvent(id, data) { + const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`) + const url = iframe.src + const eventName = 'sqlbot_assistant_event' + window.addEventListener('message', (event) => { + if (event.data?.eventName === eventName) { + if (event.data?.messageId !== id) { + return + } + if (event.data?.busi == 'ready' && event.data?.ready) { + const certificate = parsrCertificate(data) + params = { + busi: 'certificate', + certificate, + eventName, + messageId: id, + } + const contentWindow = iframe.contentWindow + contentWindow.postMessage(params, url) + } + } + }) + } + function loadScript(src, id) { + const domain_url = getDomain(src) + const online = getParam(src, 'online') + let url = `${domain_url}/api/v1/system/assistant/info/${id}` + if (domain_url.includes('5173')) { + url = url.replace('5173', '8000') + } + fetch(url) + .then((response) => response.json()) + .then((res) => { + if (!res.data) { + throw new Error(res) + } + const data = res.data + const config_json = data.configuration + let tempData = Object.assign(defaultData, data) + if (tempData.configuration) { + delete tempData.configuration + } + if (config_json) { + const config = JSON.parse(config_json) + if (config) { + delete config.id + tempData = Object.assign(tempData, config) + } + } + tempData['id'] = id + tempData['domain_url'] = domain_url + + if (tempData['float_icon'] && !tempData['float_icon'].startsWith('http://')) { + tempData['float_icon'] = + `${domain_url}/api/v1/system/assistant/picture/${tempData['float_icon']}` + + if (domain_url.includes('5173')) { + tempData['float_icon'] = tempData['float_icon'].replace('5173', '8000') + } + } + + tempData['online'] = online && online.toString().toLowerCase() == 'true' + initsqlbot_assistant(tempData) + if (data.type == 1) { + registerMessageEvent(id, tempData) + // postMessage the certificate to iframe + } + }) + .catch((e) => { + showMsg('嵌入失败', e.message) + }) + } + function getDomain(src) { + return src.substring(0, src.indexOf('/assistant.js')) + } + function init() { + const sqlbotScripts = document.querySelectorAll(`script[id^="${script_id_prefix}"]`) + const scriptsArray = Array.from(sqlbotScripts) + const src_list = scriptsArray.map((script) => script.src) + src_list.forEach((src) => { + const id = getParam(src, 'id') + window.sqlbot_assistant_handler[id] = window.sqlbot_assistant_handler[id] || {} + window.sqlbot_assistant_handler[id]['id'] = id + const propName = script_id_prefix + id + '-state' + if (window[propName]) { + return true + } + window[propName] = true + loadScript(src, id) + expposeGlobalMethods(id) + }) + } + + function showMsg(title, content) { + // 检查并创建容器(如果不存在) + let container = document.getElementById('messageContainer') + if (!container) { + container = document.createElement('div') + container.id = 'messageContainer' + container.style.position = 'fixed' + container.style.bottom = '20px' + container.style.right = '20px' + container.style.zIndex = '1000' + document.body.appendChild(container) + } else { + // 如果容器已存在,先移除旧弹窗 + const oldMessage = container.querySelector('div') + if (oldMessage) { + oldMessage.style.transform = 'translateX(120%)' + oldMessage.style.opacity = '0' + setTimeout(() => { + container.removeChild(oldMessage) + }, 300) + } + } + + // 创建弹窗元素 + const messageBox = document.createElement('div') + messageBox.style.width = '240px' + messageBox.style.minHeight = '100px' + messageBox.style.background = 'linear-gradient(135deg, #ff6b6b, #ff8e8e)' + messageBox.style.borderRadius = '8px' + messageBox.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)' + messageBox.style.padding = '15px' + messageBox.style.color = 'white' + messageBox.style.fontFamily = 'Arial, sans-serif' + messageBox.style.display = 'flex' + messageBox.style.flexDirection = 'column' + messageBox.style.transform = 'translateX(120%)' + messageBox.style.transition = 'transform 0.3s ease-out' + messageBox.style.opacity = '0' + messageBox.style.transition = 'opacity 0.3s ease, transform 0.3s ease' + messageBox.style.overflow = 'hidden' + + // 创建标题元素 + const titleElement = document.createElement('div') + titleElement.style.fontSize = '18px' + titleElement.style.fontWeight = 'bold' + titleElement.style.marginBottom = '10px' + titleElement.style.borderBottom = '1px solid rgba(255, 255, 255, 0.3)' + titleElement.style.paddingBottom = '8px' + titleElement.textContent = title + + // 创建内容元素 + const contentElement = document.createElement('div') + contentElement.style.fontSize = '14px' + contentElement.style.flexGrow = '1' + contentElement.style.overflow = 'auto' + contentElement.textContent = content + + // 组装元素 + messageBox.appendChild(titleElement) + messageBox.appendChild(contentElement) + + // 添加到容器 + container.appendChild(messageBox) + + // 触发显示动画 + setTimeout(() => { + messageBox.style.transform = 'translateX(0)' + messageBox.style.opacity = '1' + }, 10) + + // 3秒后自动隐藏 + setTimeout(() => { + messageBox.style.transform = 'translateX(120%)' + messageBox.style.opacity = '0' + setTimeout(() => { + container.removeChild(messageBox) + // 如果容器是空的,也移除容器 + if (container.children.length === 0) { + document.body.removeChild(container) + } + }, 300) + }, 5000) + } + + /* function hideMsg() { + const container = document.getElementById('messageContainer'); + if (container) { + const messageBox = container.querySelector('div'); + if (messageBox) { + messageBox.style.transform = 'translateX(120%)'; + messageBox.style.opacity = '0'; + setTimeout(() => { + container.removeChild(messageBox); + // 如果容器是空的,也移除容器 + if (container.children.length === 0) { + document.body.removeChild(container); + } + }, 300); + } + } + } */ + + function updateParam(target_url, key, newValue) { + try { + const url = new URL(target_url) + const [hashPath, hashQuery] = url.hash.split('?') + let searchParams + if (hashQuery) { + searchParams = new URLSearchParams(hashQuery) + } else { + searchParams = url.searchParams + } + searchParams.set(key, newValue) + if (hashQuery) { + url.hash = `${hashPath}?${searchParams.toString()}` + } else { + url.search = searchParams.toString() + } + return url.toString() + } catch (e) { + console.error('Invalid URL:', target_url) + return target_url + } + } + function expposeGlobalMethods(id) { + window.sqlbot_assistant_handler[id]['setOnline'] = (online) => { + if (online != null && typeof online != 'boolean') { + throw new Error('The parameter can only be of type boolean') + } + const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`) + if (iframe) { + const url = iframe.src + const eventName = 'sqlbot_assistant_event' + const params = { + busi: 'setOnline', + online, + eventName, + messageId: id, + } + const contentWindow = iframe.contentWindow + contentWindow.postMessage(params, url) + } + } + window.sqlbot_assistant_handler[id]['refresh'] = (online) => { + if (online != null && typeof online != 'boolean') { + throw new Error('The parameter can only be of type boolean') + } + const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`) + if (iframe) { + const url = iframe.src + let new_url = updateParam(url, 't', Date.now()) + if (online != null) { + new_url = updateParam(new_url, 'online', online) + } + iframe.src = 'about:blank' + setTimeout(() => { + iframe.src = new_url + }, 500) + } + } + } + // window.addEventListener('load', init) + const executeWhenReady = (fn) => { + if ( + document.readyState === 'complete' || + (document.readyState !== 'loading' && !document.documentElement.doScroll) + ) { + setTimeout(fn, 0) + } else { + const onReady = () => { + document.removeEventListener('DOMContentLoaded', onReady) + window.removeEventListener('load', onReady) + fn() + } + document.addEventListener('DOMContentLoaded', onReady) + window.addEventListener('load', onReady) + } + } + + executeWhenReady(init) +})()