195 lines
7.6 KiB
JavaScript
195 lines
7.6 KiB
JavaScript
// KI Chat Bubble — Flowise embed (Feedgine-branded)
|
|
// To remove: delete this file and its <script> tag in index.html.
|
|
|
|
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js";
|
|
|
|
// Read from config - on localhost talk directly to Flowise, on prod use proxy.
|
|
const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
|
Chatbot.init({
|
|
chatflowid: isLocal ? "d63d3d02-b5fa-482c-9161-c21c615fb625" : "chat",
|
|
apiHost: isLocal ? "https://flowise.profice.de" : window.location.origin,
|
|
theme: {
|
|
button: {
|
|
backgroundColor: "#05050d",
|
|
right: 24,
|
|
bottom: 24,
|
|
size: "medium",
|
|
iconColor: "#ff6600"
|
|
},
|
|
chatWindow: {
|
|
showTitle: true,
|
|
title: "Feedgine Assistent",
|
|
titleBackgroundColor: "#05050d",
|
|
titleTextColor: "#e8e8f2",
|
|
welcomeMessage: "Hallo! Ich bin der KI-Assistent von Feedgine.\n\nIch beantworte Ihre Fragen rund um Profit Intelligence, POAS-Optimierung und Server-Side Tracking.",
|
|
backgroundColor: "#0d0d1a",
|
|
fontSize: 15,
|
|
showAgentMessages: true,
|
|
poweredByTextColor: "#0d0d1a",
|
|
botMessage: {
|
|
backgroundColor: "rgba(255,255,255,0.07)",
|
|
textColor: "#e8e8f2",
|
|
showAvatar: true,
|
|
avatarSrc: "/images/icons/KI.png"
|
|
},
|
|
userMessage: {
|
|
backgroundColor: "rgba(255,102,0,0.18)",
|
|
textColor: "#e8e8f2",
|
|
showAvatar: false
|
|
},
|
|
textInput: {
|
|
placeholder: "Ihre Nachricht...",
|
|
backgroundColor: "rgba(255,255,255,0.04)",
|
|
textColor: "#e8e8f2",
|
|
sendButtonColor: "#ff6600"
|
|
},
|
|
footer: {
|
|
textColor: "#8888a8",
|
|
text: "KI-System · Keine sensiblen Daten eingeben.",
|
|
company: " ",
|
|
companyLink: ""
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Inject orange border around the chat header via shadow DOM
|
|
function injectHeaderStyle() {
|
|
const flowise = document.querySelector('flowise-chatbot');
|
|
if (!flowise?.shadowRoot) return false;
|
|
|
|
const shadow = flowise.shadowRoot;
|
|
if (shadow.querySelector('#feedgine-header-applied')) return true;
|
|
|
|
const all = [...shadow.querySelectorAll('*')];
|
|
const header = all.find(el => {
|
|
const bg = getComputedStyle(el).backgroundColor;
|
|
return bg === 'rgb(5, 5, 13)' && el.textContent.includes('Feedgine Assistent');
|
|
});
|
|
|
|
if (!header) return false;
|
|
|
|
header.style.borderTop = '1px solid rgba(255,102,0,0.5)';
|
|
header.style.borderLeft = '1px solid rgba(255,102,0,0.5)';
|
|
header.style.borderRight = '1px solid rgba(255,102,0,0.5)';
|
|
header.style.borderBottom = '1px solid rgba(255,102,0,0.25)';
|
|
header.style.boxShadow = '0 0 10px rgba(255,102,0,0.28), 0 0 24px rgba(255,102,0,0.10)';
|
|
header.style.boxSizing = 'border-box';
|
|
|
|
const chatWindow = header.parentElement;
|
|
if (chatWindow) {
|
|
chatWindow.style.borderBottomLeftRadius = '12px';
|
|
chatWindow.style.borderBottomRightRadius = '12px';
|
|
chatWindow.style.border = '1px solid rgba(255,102,0,0.25)';
|
|
chatWindow.style.boxShadow = '0 0 30px rgba(255,102,0,0.08)';
|
|
chatWindow.style.overflow = 'hidden';
|
|
}
|
|
|
|
const marker = document.createElement('span');
|
|
marker.id = 'feedgine-header-applied';
|
|
marker.style.display = 'none';
|
|
shadow.appendChild(marker);
|
|
return true;
|
|
}
|
|
|
|
let attempts = 0;
|
|
const interval = setInterval(() => {
|
|
if (injectHeaderStyle() || ++attempts > 20) clearInterval(interval);
|
|
}, 300);
|
|
|
|
// Keep shadow DOM cursor in sync with the main-page cursor toggle.
|
|
// CSS from the outer document cannot penetrate shadow DOM, so we inject
|
|
// a <style> tag directly and update it whenever body's class changes.
|
|
function updateShadowCursor(shadow) {
|
|
let style = shadow.querySelector('#feedgine-cursor-style');
|
|
if (!style) {
|
|
style = document.createElement('style');
|
|
style.id = 'feedgine-cursor-style';
|
|
shadow.appendChild(style);
|
|
}
|
|
style.textContent = document.body.classList.contains('system-cursor')
|
|
? ''
|
|
: '* { cursor: none !important; }';
|
|
}
|
|
|
|
function initShadowCursorSync() {
|
|
const host = document.querySelector('flowise-chatbot');
|
|
if (!host?.shadowRoot) return false;
|
|
|
|
updateShadowCursor(host.shadowRoot);
|
|
|
|
new MutationObserver(() => updateShadowCursor(host.shadowRoot))
|
|
.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
return true;
|
|
}
|
|
|
|
let cursorAttempts = 0;
|
|
const cursorInterval = setInterval(() => {
|
|
if (initShadowCursorSync() || ++cursorAttempts > 20) clearInterval(cursorInterval);
|
|
}, 300);
|
|
|
|
// Global helper: open the Flowise chat and automatically send a message.
|
|
// Called by CTA buttons across the page.
|
|
window.openChatWithMessage = function (message) {
|
|
// Defer by one tick so Flowise's own click-outside handler runs first
|
|
// (otherwise it would close the chat immediately after we open it).
|
|
setTimeout(() => {
|
|
const chatbot = document.querySelector('flowise-chatbot');
|
|
if (!chatbot || !chatbot.shadowRoot) return;
|
|
|
|
const shadow = chatbot.shadowRoot;
|
|
|
|
// Open chat window if not yet visible
|
|
if (!shadow.querySelector('textarea')) {
|
|
const toggleBtn = shadow.querySelector('button[part="button"]') || shadow.querySelector('button');
|
|
if (toggleBtn) toggleBtn.click();
|
|
}
|
|
|
|
// Poll until the textarea is available, then fill + send via Enter key
|
|
let tries = 0;
|
|
const poll = setInterval(() => {
|
|
if (++tries > 30) { clearInterval(poll); return; }
|
|
|
|
const ta = shadow.querySelector('textarea');
|
|
if (!ta) return;
|
|
clearInterval(poll);
|
|
|
|
// Use execCommand('insertText') — fires a real, trusted InputEvent
|
|
// that Solid.js signal tracking actually picks up (unlike synthetic events).
|
|
ta.focus();
|
|
ta.select();
|
|
document.execCommand('delete', false);
|
|
document.execCommand('insertText', false, message);
|
|
|
|
// Find and click the send button.
|
|
// Walk UP from the textarea; the send button is a sibling in the
|
|
// same input-row container. Fall back to the last non-toggle button
|
|
// in the entire shadow DOM (send button is always last in source order).
|
|
setTimeout(() => {
|
|
const toggleBtn = shadow.querySelector('button[part="button"]');
|
|
|
|
let sendBtn = null;
|
|
let el = ta;
|
|
for (let i = 0; i < 6 && !sendBtn; i++) {
|
|
el = el.parentElement;
|
|
if (!el) break;
|
|
const candidate = [...el.querySelectorAll('button')]
|
|
.find(b => b !== toggleBtn);
|
|
if (candidate) sendBtn = candidate;
|
|
}
|
|
|
|
// Fallback: the send button is the last button in the shadow DOM
|
|
if (!sendBtn) {
|
|
const all = [...shadow.querySelectorAll('button')]
|
|
.filter(b => b !== toggleBtn);
|
|
sendBtn = all[all.length - 1] || null;
|
|
}
|
|
|
|
if (sendBtn) sendBtn.click();
|
|
}, 300);
|
|
}, 150);
|
|
}, 200); // 200 ms lets the click-outside event fully propagate before we open
|
|
};
|