From 7907751c1dfb191e1ea81d7c4b8bc63322ee3c89 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Tue, 15 Jul 2025 15:27:52 +0800 Subject: [PATCH] perf: Assistant embed --- frontend/assistant.html | 19 -- frontend/public/assistant.js | 369 ++++++++++++++++++++++ frontend/src/main_assistant.ts | 11 - frontend/src/router/assistant.ts | 21 -- frontend/src/router/index.ts | 6 + frontend/src/router/watch.ts | 5 + frontend/src/router/watch_assistant.ts | 32 -- frontend/src/views/embedded/assistant.vue | 4 +- frontend/vite.config.ts | 4 - 9 files changed, 383 insertions(+), 88 deletions(-) delete mode 100644 frontend/assistant.html create mode 100644 frontend/public/assistant.js delete mode 100644 frontend/src/main_assistant.ts delete mode 100644 frontend/src/router/assistant.ts delete mode 100644 frontend/src/router/watch_assistant.ts diff --git a/frontend/assistant.html b/frontend/assistant.html deleted file mode 100644 index c65334fd..00000000 --- a/frontend/assistant.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - SQLBot - - -
- - - - diff --git a/frontend/public/assistant.js b/frontend/public/assistant.js new file mode 100644 index 00000000..5b3d0fda --- /dev/null +++ b/frontend/public/assistant.js @@ -0,0 +1,369 @@ +;(function () { + 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_value: '-5', + y_value: '33', + } + 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') + const chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img') + // 对话框元素 + 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') + } + } + const drag = (e) => { + if (['touchmove', 'touchstart'].includes(e.type)) { + chat_button.style.top = e.touches[0].clientY - chat_button_img.naturalHeight / 2 + 'px' + chat_button.style.left = e.touches[0].clientX - chat_button_img.naturalWidth / 2 + 'px' + } else { + chat_button.style.top = e.y - chat_button_img.naturalHeight / 2 + 'px' + chat_button.style.left = e.x - chat_button_img.naturalWidth / 2 + 'px' + } + chat_button.style.width = chat_button_img.naturalWidth + 'px' + chat_button.style.height = chat_button_img.naturalHeight + 'px' + } + if (data.is_draggable) { + 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_value}px; + ${data.y_type}: ${data.y_value}px; + z-index: 10001; + } + #sqlbot-assistant .sqlbot-assistant-tips { + position: fixed; + ${data.x_type}:calc(${data.x_value}px + 75px); + ${data.y_type}: calc(${data.y_value}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: 450px; + height: 600px; + 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_value}px; + ${data.y_type}: ${data.y_value}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; + } + + #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{ + 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 loadScript(src, id) { + const domain_url = getDomain(src) + // const url = `${domain_url}api/v1/system/assistant/validator/${id}` + /* const url = `http://localhost:8000/api/v1/system/assistant/validator/${id}` + fetch(url) + .then((response) => response.json()) + .then((data) => { + console.log(data) + const tempData = Object.assign(defaultData, data) + initsqlbot_assistant(tempData) + }) */ + initsqlbot_assistant(defaultData) + } + 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') + const propName = script_id_prefix + id + '-state' + if (window[propName]) { + return true + } + window[propName] = true + loadScript(src, id) + }) + } + window.addEventListener('load', init) +})() diff --git a/frontend/src/main_assistant.ts b/frontend/src/main_assistant.ts deleted file mode 100644 index b458a6c6..00000000 --- a/frontend/src/main_assistant.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from 'vue' -import './style.less' -import App from './App.vue' -import router from './router/assistant' -import { i18n } from './i18n' - -const app = createApp(App) - -app.use(router) -app.use(i18n) -app.mount('#sqlbot_embedded_app') diff --git a/frontend/src/router/assistant.ts b/frontend/src/router/assistant.ts deleted file mode 100644 index fec2ddc4..00000000 --- a/frontend/src/router/assistant.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createRouter, createWebHashHistory } from 'vue-router' - -import assistant from '@/views/embedded/assistant.vue' -import { watchRouter } from './watch_assistant' -const router = createRouter({ - history: createWebHashHistory(), - routes: [ - { - path: '/:id', - name: 'index', - component: assistant, - }, - { - path: '/assistant:id', - name: 'assistant', - component: assistant, - }, - ], -}) -watchRouter(router) -export default router diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index c647d9f9..7a56a075 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -10,6 +10,7 @@ import DashboardEditor from '@/views/dashboard/editor/index.vue' import DashboardPreview from '@//views/dashboard/preview/SQPreviewSingle.vue' import Dashboard from '@/views/dashboard/index.vue' import Model from '@/views/system/model/Model.vue' +import assistant from '@/views/embedded/assistant.vue' // import User from "@/views/system/user/index.vue"; import { watchRouter } from './watch' const router = createRouter({ @@ -128,6 +129,11 @@ const router = createRouter({ }, ], }, + { + path: '/assistant', + name: 'assistant', + component: assistant, + }, ], }) watchRouter(router) diff --git a/frontend/src/router/watch.ts b/frontend/src/router/watch.ts index b359c2ef..14bf2ad9 100644 --- a/frontend/src/router/watch.ts +++ b/frontend/src/router/watch.ts @@ -5,9 +5,14 @@ import { request } from '@/utils/request' const userStore = useUserStore() const { wsCache } = useCache() const whiteList = ['/login'] +const assistantWhiteList = ['/assistant'] export const watchRouter = (router: any) => { router.beforeEach(async (to: any, from: any, next: any) => { await loadXpackStatic() + if (assistantWhiteList.includes(to.path)) { + next() + return + } const token = wsCache.get('user.token') if (whiteList.includes(to.path)) { next() diff --git a/frontend/src/router/watch_assistant.ts b/frontend/src/router/watch_assistant.ts deleted file mode 100644 index 1e0fcbcb..00000000 --- a/frontend/src/router/watch_assistant.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ElMessage } from 'element-plus-secondary' - -import { request } from '@/utils/request' - -export const watchRouter = (router: any) => { - router.beforeEach(async (to: any, from: any, next: any) => { - console.log(to, from) - await loadXpackStatic() - next() - }) -} -const loadXpackStatic = () => { - if (document.getElementById('sqlbot_xpack_static')) { - return Promise.resolve() - } - const url = '/xpack_static/license-generator.umd.js' - return new Promise((resolve, reject) => { - request - .loadRemoteScript(url, 'sqlbot_xpack_static', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - LicenseGenerator?.init(import.meta.env.VITE_API_BASE_URL).then(() => { - resolve(true) - }) - }) - .catch((error) => { - console.error('Failed to load xpack_static script:', error) - ElMessage.error('Failed to load license generator script') - reject(error) - }) - }) -} diff --git a/frontend/src/views/embedded/assistant.vue b/frontend/src/views/embedded/assistant.vue index 199940db..bede90c7 100644 --- a/frontend/src/views/embedded/assistant.vue +++ b/frontend/src/views/embedded/assistant.vue @@ -95,7 +95,9 @@ const validator = ref({ }) const loading = ref(true) onBeforeMount(async () => { - const assistantId = route.params.id + const assistantId = route.query.id + const online = route.query.online + console.log(online) const now = Date.now() assistantStore.setFlag(now) const id = `${assistantId}-${assistantStore.getFlag}` diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8e88d435..4749a7e2 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -39,10 +39,6 @@ export default defineConfig(({ mode }) => { build: { chunkSizeWarningLimit: 2000, rollupOptions: { - input: { - main: path.resolve(__dirname, 'index.html'), - assistant: path.resolve(__dirname, 'assistant.html'), - }, output: { manualChunks: { 'element-plus-secondary': ['element-plus-secondary'],