Passa al contenuto principale

Sistema Widget Preact e Shadow DOM

Il sistema di widget di Tidiko AI è stato completamente riscritto utilizzando Preact per fornire un'integrazione moderna, performante e completamente isolata in qualsiasi sito web. Il widget utilizza Shadow DOM per garantire l'isolamento completo dagli stili e dal JavaScript della pagina host.

🌐 Architettura del Widget Preact

Shadow DOM + Preact Implementation

Il nuovo widget combina Shadow DOM con Preact per un'esperienza di sviluppo moderna e performante:

resources/js/shadowDomWidgetPreact.jsx
import { render, Fragment, h } from "preact";
import { memo } from "preact/compat";
import {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from "preact/hooks";

// Custom Shadow DOM renderer for Preact
function createShadowRenderer(shadowRoot) {
return {
render: (component) => render(component, shadowRoot),
};
}

class TidikoAiWidgetPreact {
constructor(containerId = null, params = {}) {
// Configurazione del widget
this.options = { ...platformSettings, ...overrideSettings };
this.jwt = jwt;
this.appUrl = appUrl;
this.type = type;

// Inizializzazione
this.init(containerId);
}

async init(containerId) {
// Creazione del container
const host = containerId
? document.getElementById(containerId)
: this._createDefaultContainer();

if (!host) {
console.error("Container not found");
return;
}

// Attach Shadow DOM
this.shadowRoot = host.attachShadow({ mode: "open" });
await this._injectContent();
this._renderWidget();
}

_renderWidget() {
// Create shadow DOM renderer and render the widget
const renderer = createShadowRenderer(this.shadowRoot);
renderer.render(
<TidikoWidget
options={this.options}
payload={this.payload}
collectionName={this.collectionName}
// ... altri props
/>
);
}
}

Architettura a Componenti Preact

Il widget è ora strutturato come una gerarchia di componenti Preact ottimizzati:

resources/js/shadowDomWidgetPreact.jsx
// Componente principale del widget
const TidikoWidget = ({
options,
payload,
collectionName,
companyName,
// ... altri props
}) => {
// State management con hooks
const [currentThreadId, setCurrentThreadId] = useState(null);
const [isRunning, setIsRunning] = useState(false);
const [messages, setMessages] = useState([]);
const [currentMessage, setCurrentMessage] = useState("");
const [showTools, setShowTools] = useState(false);
const [showPrivacy, setShowPrivacy] = useState(false);
const [showFAQ, setShowFAQ] = useState(false);
// ... altri state

return (
<div className={wrapperClass} style={wrapperStyle}>
<div className={containerClass}>
<ChatHeader
options={options}
showTools={showTools && !isCollapsed}
copyMessage={copyMessage}
onCopyChat={handleCopyChat}
onTrashChat={handleClearChat}
onCloseChat={handleCloseChat}
/>

<ChatHistory
messages={messages}
showFAQ={showFAQ && !isCollapsed}
faqQuestions={options.faqMessages}
showPrivacy={showPrivacy}
options={options}
isRunning={isRunning}
linksTarget={linksTarget}
onFAQQuestionClick={handleFAQQuestionClick}
onPrivacyAccept={handlePrivacyAccept}
onScroll={handleScroll}
onLinkClick={handleLinkClick}
language={language}
isHistoryLoading={isHistoryLoading}
/>

<MessageForm
message={currentMessage}
placeholder={placeholder}
isRunning={isRunning}
isSocketConnected={isSocketConnected}
onMessageChange={handleMessageChange}
onSubmit={handleSubmit}
onKeyDown={handleKeyDown}
onFocus={handleShowChatHistory}
privacyAccepted={showPrivacy}
/>

<div className="chat-footer">
<div className="chat-copyright">
Powered by{" "}
<a
href="https://tidiko.ai"
target="_blank"
rel="noopener"
>
Tidiko.ai
</a>
</div>
</div>
</div>

<button className="open-chat-toggle" onClick={handleToggleChat}>
<ChatToggleIcon />
</button>
</div>
);
};

🧩 Componenti Preact

Componenti Icone

Tutti i componenti icona sono ottimizzati con memo() per evitare re-render inutili:

resources/js/shadowDomWidgetPreact.jsx
const DefaultIcon = memo(() => (
<svg
width="30"
height="30"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="default-icon"
>
<rect width="40" height="40" rx="20" fill="#E97222" />
<path
d="M14.7611 29.1869C15.8673 29.1869 16.9443 28.9796 17.9922 28.5352C18.9818 28.1205 19.8551 27.4985 20.6119 26.7283C21.3687 25.9581 21.98 25.0399 22.3875 24.0327C22.8241 22.996 23.057 21.9 23.057 20.7447C23.057 19.6191 22.8241 18.5231 22.3875 17.4567C21.98 16.4495 21.3687 15.5609 20.6119 14.7907C19.8551 14.0206 18.9527 13.3985 17.9922 12.9838C16.9443 12.5395 15.8673 12.3025 14.7611 12.3025C13.6259 12.3025 12.5489 12.5395 11.5302 12.9838C10.5405 13.3985 9.63813 14.0206 8.88132 14.7907C8.1245 15.5609 7.51322 16.4792 7.10571 17.4567C6.66908 18.5231 6.46533 19.6191 6.46533 20.7447C6.46533 21.9 6.66908 22.996 7.10571 24.0327C7.51322 25.0399 8.1245 25.9581 8.88132 26.7283C9.63813 27.4985 10.5405 28.1205 11.5302 28.5352C12.5489 28.9796 13.6259 29.1869 14.7611 29.1869ZM14.7611 16.7458C16.9152 16.7458 18.6908 18.5527 18.6908 20.7447C18.6908 22.9663 16.9152 24.7437 14.7611 24.7437C12.578 24.7437 10.8316 22.9663 10.8316 20.7447C10.8316 18.5527 12.578 16.7458 14.7611 16.7458Z"
fill="white"
/>
<path
d="M24.5352 20.6157L32.5344 20.6363"
stroke="white"
stroke-width="3"
/>
<path
d="M23.884 25.0739L30.7687 28.4763"
stroke="white"
stroke-width="3"
/>
<path
d="M30.2566 11.4457L23.811 15.6454"
stroke="white"
stroke-width="3"
/>
</svg>
));

const CopyIcon = memo(() => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.8 18C17.9201 18 18.4802 18 18.908 17.782C19.2843 17.5903 19.5903 17.2843 19.782 16.908C20 16.4802 20 15.9201 20 14.8V5.2C20 4.0799 20 3.51984 19.782 3.09202C19.5903 2.71569 19.2843 2.40973 18.908 2.21799C18.4802 2 17.9201 2 16.8 2H11.2C10.0799 2 9.51984 2 9.09202 2.21799C8.71569 2.40973 8.40973 2.71569 8.21799 3.09202C8 3.51984 8 4.07989 8 5.2M7.2 22H12.8C13.9201 22 14.4802 22 14.908 21.782C15.2843 21.5903 15.5903 21.2843 15.782 20.908C16 20.4802 16 19.9201 16 18.8V9.2C16 8.07989 16 7.51984 15.782 7.09202C15.5903 6.71569 15.2843 6.40973 14.908 6.21799C14.4802 6 13.9201 6 12.8 6H7.2C6.0799 6 5.51984 6 5.09202 6.21799C4.71569 6.40973 4.40973 6.71569 4.21799 7.09202C4 7.51984 4 8.07989 4 9.2V18.8C4 19.9201 4 20.4802 4.21799 20.908C4.40973 21.2843 4.71569 21.5903 5.09202 21.782C5.51984 22 6.07989 22 7.2 22Z"
stroke="#7C7979"
stroke-width="2"
/>
</svg>
));

Componente ProductCard

Nuovo componente per la visualizzazione di prodotti con supporto per skeleton loading:

resources/js/shadowDomWidgetPreact.jsx
const ProductCard = memo(
({
id,
url,
addToCartUrl,
imageSrc,
imageAlt,
productTitle,
originalPrice,
discountPrice,
savings,
savingsPercentage,
variantsHtml,
isSkeleton = false,
language = "italian",
}) => {
const hasImage = Boolean(imageSrc);
const hasTitle = Boolean(productTitle);
const hasAnyPrice = Boolean(discountPrice || originalPrice || savings);

return (
<div className={`product-card ${isSkeleton ? "skeleton" : ""}`}>
<div className="product-image-container-wrapper">
<div className="product-image-container">
{hasImage ? (
<img
src={imageSrc}
alt={imageAlt}
className="product-image"
onError={(e) =>
(e.target.style.display = "none")
}
/>
) : (
<div className="skeleton-block skeleton-image" />
)}
</div>
</div>
<div className="product-content">
{hasTitle ? (
<h3 className="product-title">{productTitle}</h3>
) : (
<div className="skeleton-block skeleton-title" />
)}
<div className="product-price">
{discountPrice ? (
<span className="product-price-discount">
{discountPrice}
</span>
) : (
<div className="skeleton-block skeleton-price" />
)}
{originalPrice ? (
<span className="product-price-original">
{originalPrice}
</span>
) : hasAnyPrice ? null : (
<div className="skeleton-block skeleton-price small" />
)}
{savings ? (
<span
className="product-price-savings"
aria-label={`${__(
"product_price_savings_aria_label",
getLanguage(language) || "italian"
)} ${savings} (${savingsPercentage})`}
>
{savings} € ({savingsPercentage})
</span>
) : null}
</div>
{variantsHtml ? (
<div
className="product-variants"
dangerouslySetInnerHTML={{ __html: variantsHtml }}
/>
) : (
<div className="product-variants">
<div className="skeleton-block skeleton-variant" />
<div className="skeleton-block skeleton-variant short" />
</div>
)}
<div className="product-actions">
{addToCartUrl ? (
<a href={addToCartUrl} className="btn btn-primary">
{__(
"product_add_to_cart",
getLanguage(language) || "italian"
)}
</a>
) : (
<div className="btn btn-primary skeleton-block skeleton-button" />
)}
{url ? (
<a href={url} className="btn btn-outline">
{__(
"product_details",
getLanguage(language) || "italian"
)}
</a>
) : (
<div className="btn btn-outline skeleton-block skeleton-button" />
)}
</div>
</div>
</div>
);
}
);

Componente Message

Componente ottimizzato per la gestione dei messaggi con supporto per markdown e product cards:

resources/js/shadowDomWidgetPreact.jsx
const Message = memo(
({
type,
content,
textClass,
isBreathing = false,
language = "italian",
}) => {
const messageClass = `message ${type}-message ${textClass} ${
isBreathing ? "shimmer" : ""
}`;

if (type === "loading") {
return <LoadingIndicator textClass={textClass} />;
}

if (type === "system") {
// Extract product cards data BEFORE processing markdown
const { content: processedContent, productCards } =
extractProductCardsData(content);

return (
<div className={messageClass}>
<div
dangerouslySetInnerHTML={{
__html: marked.parse(processedContent),
}}
/>
{productCards.map((cardData, index) => (
<ProductCard
key={`${cardData.id}-${index}`}
{...cardData}
language={language}
/>
))}
</div>
);
}

return <div className={messageClass}>{content}</div>;
}
);

Componente FAQ

Sistema FAQ migliorato con supporto per skeleton loading:

resources/js/shadowDomWidgetPreact.jsx
const FAQWrapper = memo(
({
questions,
onQuestionClick,
isRunning,
isInitial = false,
isSkeleton = false,
language = "italian",
}) => {
if (!isSkeleton && (!questions || questions.length === 0)) return null;

if (isSkeleton) {
return (
<div className={`faq-wrapper ${isInitial ? "initial" : ""}`}>
<p className="faq-title">{__("try_asking", language)}</p>
<div className="faq-container faq-skeleton">
<div className="skeleton-chip" />
<div className="skeleton-chip" />
<div className="skeleton-chip short" />
</div>
</div>
);
}

return (
<div className={`faq-wrapper ${isInitial ? "initial" : ""}`}>
<p className="faq-title">{__("try_asking", language)}</p>
<div className="faq-container">
{questions.map((question, index) => (
<FAQButton
key={index}
question={question}
onClick={onQuestionClick}
isRunning={isRunning}
/>
))}
</div>
</div>
);
}
);

🎨 Sistema di Stili

CSS Variables per Personalizzazione

Il widget utilizza CSS variables per permettere la personalizzazione dinamica:

resources/scss/agentWidget.scss
.chat-wrapper {
--default-button-bg: #ff8e5e;
--default-button-color: #000;
--primary-color: #0f0f0f;
--primary-light-color: #707070;
--border-color: #e5e7eb;
--default-user-message-bg: #ff8e5e;
--default-assistant-message-bg: #f3f4f6;
--user-message-color: #000;
--assistant-message-color: #000;
--button-bg: #ff8e5e;
--button-color: #000;
--faq-bg-color: rgb(255 142 94 / 1);
--faq-bg-color-hover: rgb(255 142 94 / 0.7);
}

Temi Predefiniti

resources/js/shadowDomWidget.js
_applyConfiguration() {
// Titolo
this.dom.chatTitle.textContent = this.options.chatTitle || "Tidiko Chat";

// Placeholder
this.dom.messageInput.placeholder = this.options.inputPlaceholder || "Chiedimi qualcosa";

// Logo personalizzato
if (this.options.useCustomLogo && this.options.logoUrl) {
this.dom.customIcon.src = this.options.logoUrl;
this.dom.customIcon.parentElement.classList.add("custom");
}

// Tema
if (this.options.colorScheme === "dark") {
this.dom.container.classList.add("dark");
}

// Colori personalizzati
const setVar = (varName, value) =>
this.dom.wrapper.style.setProperty(varName, value);

if (this.options.userBg) {
setVar("--user-message-bg", this.options.userBg);
const userTextColor = this.getContrastColor(this.options.userBg);
setVar("--user-message-color", userTextColor);
}

if (this.options.aiBg) {
setVar("--assistant-message-bg", this.options.aiBg);
const assistantTextColor = this.getContrastColor(this.options.aiBg);
setVar("--assistant-message-color", assistantTextColor);
}
}

🔌 Sistema di Eventi e Hooks

Gestione Eventi con Preact Hooks

Il nuovo widget utilizza Preact hooks per una gestione degli eventi più moderna e reattiva:

resources/js/shadowDomWidgetPreact.jsx
// Event handlers con useCallback per ottimizzazione
const handleSubmit = useCallback(
async (e) => {
e.preventDefault();
if (!currentMessage.trim() || isRunning) return;

// Clear input field and send message
const text = currentMessage.trim();
setCurrentMessage("");
await sendMessage(text);
},
[currentMessage, isRunning, sendMessage]
);

const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
if (!inputDisabled && !isRunning && currentMessage.trim()) {
handleSubmit(e);
}
}
},
[handleSubmit, inputDisabled, isRunning, currentMessage]
);

const handleFAQQuestionClick = useCallback(
async (question) => {
if (isRunning || inputDisabled || !question.trim()) return;

// Hide FAQ immediately when question is clicked
setShowFAQ(false);

// Send the FAQ question directly without setting currentMessage
await sendMessage(question.trim());
},
[isRunning, inputDisabled, sendMessage]
);

const handleCopyChat = useCallback(() => {
const text = messages
.filter((msg) => msg.type !== "loading")
.map((msg) =>
msg.type === "user"
? "Tu: " + msg.content
: "Assistente: " + msg.content
)
.join("\n\n");

navigator.clipboard.writeText(text).then(() => {
setCopyMessage("Chat copiata");
setTimeout(() => setCopyMessage(""), 4000);
});
}, [messages]);

const handleScroll = useCallback((e) => {
const { scrollTop, clientHeight, scrollHeight } = e.target;
setAutoScrollEnabled(scrollTop + clientHeight >= scrollHeight - 10);
}, []);

WebSocket Communication con useEffect

Il nuovo widget utilizza useEffect per gestire la connessione WebSocket in modo reattivo:

resources/js/shadowDomWidgetPreact.jsx
// Socket setup effect
useEffect(() => {
const setupSocket = () => {
const token = window.localStorage.getItem("tidiko_jwt");
socketRef.current = io(socketUrl, {
auth: { token },
reconnection: true,
reconnectionDelay: 1000,
});

const socket = socketRef.current;

socket.on("disconnect", () => {
setIsSocketConnected(false);
setMessages((prev) => [
...prev,
{
type: "system",
content: "Connessione persa. Riconnessione in corso...",
textClass: assistantTextClass,
},
]);
});

socket.on("connect", () => {
setIsSocketConnected(true);
setMessages((prev) =>
prev.filter(
(msg) => !msg.content.includes("Connessione persa")
)
);
});

socket.on("chat_response_start", () => {
// Remove breathing from any tool_feedback messages and remove loading indicators
setMessages((prev) => {
return prev
.filter((msg) => msg.type !== "loading")
.map((msg) => ({
...msg,
isBreathing: false,
}));
});
setTmpMsg("");
setTmpMsgType("chat_response");
currentSystemMessageRef.current = null;

// Re-enable input as soon as response starts
setIsRunning(false);
});

socket.on("chat_response_chunk", ({ response }) => {
// Remove loading indicators when receiving response chunks
setMessages((prev) =>
prev.filter((msg) => msg.type !== "loading")
);
setTmpMsg((prev) => prev + response);
setTmpMsgType("chat_response");
setIsRunning(true);
});

socket.on("chat_response_end", () => {
// Process the last system message for questions
setMessages((prev) => {
const newMessages = [...prev];
const lastIndex = newMessages.length - 1;

if (
lastIndex >= 0 &&
newMessages[lastIndex].type === "system" &&
newMessages[lastIndex].isStreaming
) {
const lastMessage = newMessages[lastIndex];
const processed = processMessageContent(
lastMessage.content
);

newMessages[lastIndex] = {
...lastMessage,
content: processed.content,
questions: processed.questions,
showFAQ: processed.showFAQ,
showSkeletonFAQ: false,
isStreaming: false,
isBreathing: false,
};
}

// Ensure only the last message with questions shows FAQ
return ensureOnlyLastFAQVisible(newMessages);
});

setTmpMsg("");
setTmpMsgType("");
currentSystemMessageRef.current = null;
setIsRunning(false);
});

socket.on("tool_feedback", ({ feedback }) => {
// Remove loading indicators when receiving tool feedback
setMessages((prev) =>
prev.filter((msg) => msg.type !== "loading")
);
setTmpMsg(cleanTmpMsg(feedback));
setTmpMsgType("tool_feedback");
});

socket.on("thread_created", ({ threadId }) => {
setCurrentThreadId(threadId);
const key = `tidiko_thread_${widgetAssistantId}`;
window.localStorage.setItem(key, threadId);
});

socket.on("thread_messages", (data) => {
const msgs = Array.isArray(data.messages) ? data.messages : [];
if (options.defaultMessage) {
msgs.unshift({
type: "system",
content: options.defaultMessage,
});
}

const formattedMessages = msgs.map((m) => {
const baseMessage = {
type: m.type === "user" ? "user" : "system",
textClass:
m.type === "user"
? userTextClass
: assistantTextClass,
};

// Process system messages for questions
if (m.type !== "user") {
const processed = processMessageContent(m.content);
return {
...baseMessage,
content: processed.content,
questions: processed.questions,
showFAQ: processed.showFAQ,
showSkeletonFAQ: processed.showSkeletonFAQ,
};
}

return {
...baseMessage,
content: m.content,
};
});

// Ensure only the last message with questions shows FAQ
const messagesWithCorrectFAQVisibility =
ensureOnlyLastFAQVisible(formattedMessages);

// Store loaded messages but only show them if privacy is accepted
setLoadedMessages(messagesWithCorrectFAQVisibility);
setIsHistoryLoading(false);

// Check if privacy is accepted to show messages
const accepted = document.cookie.includes(
"tidiko_widget_privacy_accepted="
);
if (accepted) {
setMessages(messagesWithCorrectFAQVisibility);
setShowTools(true);

// Hide FAQ if there are existing messages (excluding default message)
if (data.messages && data.messages.length > 0) {
setShowFAQ(false);
} else {
setShowFAQ(true);
}
} else {
// Privacy not accepted - don't show messages yet
setShowFAQ(false);
}
});

socket.on("error", (error) => {
setMessages((prev) => [
...prev,
{
type: "system",
content: `Mi dispiace, si è verificato un errore: ${
error.error || "Unknown"
}`,
textClass: assistantTextClass,
},
]);
setIsRunning(false);
});
};

setupSocket();

return () => {
if (socketRef.current) {
socketRef.current.disconnect();
}
};
}, [
socketUrl,
assistantTextClass,
userTextClass,
options.defaultMessage,
widgetAssistantId,
cleanTmpMsg,
processMessageContent,
ensureOnlyLastFAQVisible,
]);

💬 Gestione Messaggi con Preact

Invio Messaggi con useCallback

Il nuovo widget utilizza useCallback per ottimizzare la funzione di invio messaggi:

resources/js/shadowDomWidgetPreact.jsx
// Helper function to send a message
const sendMessage = useCallback(
async (messageText) => {
if (!messageText.trim() || isRunning) return;

const text = messageText.trim();

// Hide FAQ
setShowFAQ(false);

// Hide all previous FAQ and add user message
setMessages((prev) => {
// First hide all FAQ from previous messages
const messagesWithHiddenFAQ = prev.map((msg) => ({
...msg,
showFAQ: false,
}));

// Add the new user message
return [
...messagesWithHiddenFAQ,
{
type: "user",
content: text,
textClass: userTextClass,
},
];
});

// Add loading indicator
setMessages((prev) => [
...prev,
{
type: "loading",
textClass: assistantTextClass,
},
]);

setIsRunning(true);

// Ensure thread exists and get the actual threadId to use
let actualThreadId = currentThreadId;

let agentId = widgetAssistantId;
if (!agentId.split("_")[1]) {
agentId =
type === "assistant"
? `assistant_${agentId}`
: `agent_${agentId}`;
}

if (!actualThreadId) {
socketRef.current.emit("create_thread", {
agentId,
company_name: companyName,
company_description: companyDescription,
language: language,
laravelAppUrl: appUrl,
type: type,
});

// Wait for thread creation and get the threadId
actualThreadId = await new Promise((resolve) => {
const handleThreadCreated = ({ threadId }) => {
setCurrentThreadId(threadId);
socketRef.current.off(
"thread_created",
handleThreadCreated
);
resolve(threadId); // Return the threadId directly
};
socketRef.current.on("thread_created", handleThreadCreated);
});
}

// Send message with the actual threadId
const msgObj = {
message: text,
threadId: actualThreadId, // Use the actual threadId instead of state
agentId: widgetAssistantId,
collection_name: collectionName,
companyName: companyName,
companyDescription: companyDescription,
baseFeedUrl: baseFeedUrl,
hasDocuments: hasDocuments,
language: language,
laravelAppUrl: appUrl,
websiteUrl: websiteUrl,
type: type,
assistantInstructions: assistantInstructions,
};

if (payload) msgObj.payload = payload;
lastFailedMessageRef.current = msgObj;
socketRef.current.emit("chat_message", msgObj);

// Loading indicator will be removed when chat_response_end is received
},
[
isRunning,
currentThreadId,
userTextClass,
assistantTextClass,
widgetAssistantId,
collectionName,
companyName,
companyDescription,
baseFeedUrl,
hasDocuments,
language,
appUrl,
websiteUrl,
type,
assistantInstructions,
payload,
setShowFAQ,
setMessages,
setIsRunning,
setInputDisabled,
setCurrentThreadId,
ensureOnlyLastFAQVisible,
]
);

Rendering Messaggi

resources/js/shadowDomWidget.js
addMessageToChat(type, content, isHistory = false) {
const frag = document.createDocumentFragment();
const msgDiv = this.createMessageElement(type, content);
frag.appendChild(msgDiv);

if (type === "user" && !isHistory) {
frag.appendChild(this.createLoadingIndicator());
}

this.dom.chatHistory.appendChild(frag);
this.scrollChatToBottomIfNeeded();
return msgDiv;
}

createMessageElement(role, content) {
const div = document.createElement("div");
div.className = `message ${role}-message`;

// Aggiunta classi colore testo
if (role === "user" && this.userTextClass) {
div.classList.add(this.userTextClass);
} else if (role === "system" && this.assistantTextClass) {
div.classList.add(this.assistantTextClass);
}

if (role === "system") {
div.innerHTML = marked.parse(this.cleanTmpMsg(content));
} else {
div.textContent = content;
}

return div;
}

🎯 Sistema FAQ

Gestione Domande Frequenti

resources/js/shadowDomWidget.js
_initFAQ() {
if (!this.options.faqMessages ||
!this.options.faqMessages.length ||
this.currentThreadId) {
return;
}

const wrapper = document.createElement("div");
wrapper.classList.add("faq-wrapper", "initial");
this.displayQuestions(this.options.faqMessages, wrapper);
this.dom.chatHistory.appendChild(wrapper);
}

displayQuestions(questions, wrapper) {
const title = document.createElement("p");
title.classList.add("faq-title");
title.textContent = "Prova a chiedere";
wrapper.appendChild(title);

const container = document.createElement("div");
container.classList.add("faq-container");

for (const q of questions) {
const btn = document.createElement("button");
btn.type = "button";
btn.classList.add("faq-single");
btn.textContent = q;
btn.addEventListener("click", () => {
if (this.isRunning) return;
this.dom.messageInput.value = q;
this.sendMessage();
wrapper.remove();
});
container.appendChild(btn);
}

wrapper.appendChild(container);
}

🔧 Configurazione Avanzata Preact

Opzioni di Configurazione

resources/js/shadowDomWidgetPreact.jsx
const defaultOptions = {
// Impostazioni base
chatTitle: "Tidiko.ai Chat",
inputPlaceholder: "Chiedimi qualcosa",
defaultMessage: null,
language: "Italian",

// Configurazione API
appUrl: null,
websiteUrl: null,
jwt: null,
jwtRefresh: null,
socketUrl: null,

// Personalizzazione aspetto
colorScheme: "light",
primaryColor: "#ff8e5e",
loadInButton: true,
buttonPosition: "right",
useCustomLogo: false,
logoUrl: null,
userBg: null,
aiBg: null,
buttonColor: null,

// Configurazione chat
type: "assistant",
hasDocuments: false,
assistantInstructions: null,
widgetAssistantId: null,
fullHeight: false,
linksTarget: "self",

// Informazioni azienda
companyName: null,
companyDescription: null,
collectionName: null,
baseFeedUrl: null,

// FAQ predefinite
faqMessages: [],

// Privacy e GDPR
privacyEndpoint: null,
csrfToken: null,
translations: {
privacy: {
message: "By using Tidiko AI Chat, you accept our",
policyLink: "Privacy Policy",
acceptButton: "OK"
}
},

// Override avanzate
overrideSettings: {},
platformSettings: {},
payload: null,
};

API Pubbliche del Widget Preact

resources/js/shadowDomWidgetPreact.jsx
// Inizializzazione del widget
const widget = new TidikoAiWidgetPreact(containerId, {
// Opzioni di configurazione
platformSettings: {
chatTitle: "Il Mio Chatbot",
inputPlaceholder: "Come posso aiutarti?",
colorScheme: "dark",
userBg: "#ff6b35",
aiBg: "#f8f9fa",
buttonColor: "#007bff",
useCustomLogo: true,
logoUrl: "https://example.com/logo.png",
faqMessages: [
"Quali sono i vostri orari?",
"Come posso contattarvi?",
"Offrite servizi di consegna?"
]
},

// Configurazione API
appUrl: "https://api.example.com",
websiteUrl: "https://example.com",
jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
jwtRefresh: "refresh_token_here",
socketUrl: "wss://socket.example.com",

// Configurazione chat
type: "assistant",
widgetAssistantId: "assistant_123",
language: "Italian",
hasDocuments: true,
assistantInstructions: "Sei un assistente virtuale specializzato...",

// Informazioni azienda
companyName: "La Mia Azienda",
companyDescription: "Descrizione della mia azienda...",
collectionName: "prodotti",
baseFeedUrl: "https://api.example.com/feed",

// Opzioni avanzate
fullHeight: false,
linksTarget: "_blank",
payload: { userId: "123", sessionId: "abc" }
});

// Metodi pubblici disponibili
widget.updateOptions({
chatTitle: "Nuovo Titolo",
userBg: "#ff0000"
});

widget.destroy(); // Rimuove il widget dalla pagina

Metodi Pubblici Preact

resources/js/shadowDomWidgetPreact.jsx
class TidikoAiWidgetPreact {
// Aggiorna configurazione
updateOptions(newOpts) {
Object.assign(this.options, newOpts);
this._renderWidget(); // Re-render with new options
}

// Distruggi widget
destroy() {
if (this.shadowRoot && this.shadowRoot.host) {
this.shadowRoot.host.remove();
}
}

// Metodi interni per la gestione del widget
_createDefaultContainer() {
const c = document.createElement("div");
c.id = "tidiko-ai-widget";
c.style.cssText = "position:fixed;bottom:0;right:0;z-index:999999;";
document.body.appendChild(c);
return c;
}

async _injectContent() {
// Styles
if (isDevelopment) {
await this._loadDevStyles();
} else {
const s = document.createElement("style");
s.textContent = CSS_STYLES;
this.shadowRoot.appendChild(s);
}
}

async _loadDevStyles() {
try {
const devUrl =
import.meta.env.VITE_DEV_SERVER_URL || "http://localhost:5173";
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `${devUrl}/resources/scss/agentWidget.scss`;
this.shadowRoot.appendChild(link);
} catch (e) {
console.warn("Dev styles failed, using fallback", e);
this._injectFallbackStyles();
}
}

_injectFallbackStyles() {
const style = document.createElement("style");
style.textContent = `
/* fallback CSS */
.chat-wrapper { font-family: sans-serif; width:350px; height:500px; }
.hidden{display:none!important;}
/* ... */
`;
this.shadowRoot.appendChild(style);
}

_renderWidget() {
// Create shadow DOM renderer and render the widget
const renderer = createShadowRenderer(this.shadowRoot);
renderer.render(
<TidikoWidget
options={this.options}
payload={this.payload}
collectionName={this.collectionName}
companyName={this.companyName}
companyDescription={this.companyDescription}
baseFeedUrl={this.baseFeedUrl}
hasDocuments={this.hasDocuments}
language={this.language}
jwt={this.jwt}
jwtRefresh={this.jwtRefresh}
appUrl={this.appUrl}
websiteUrl={this.websiteUrl}
assistantInstructions={this.assistantInstructions}
type={this.type}
fullHeight={this.fullHeight}
socketUrl={this.socketUrl}
widgetAssistantId={this.widgetAssistantId}
linksTarget={this.linksTarget}
/>
);
}
}

// Auto-inizializzazione con data attributes
document.addEventListener("DOMContentLoaded", () => {
const el = document.querySelector("[data-tidiko-ai-widget-preact]");
if (el) {
const params = JSON.parse(el.dataset.tidikoAiWidgetPreact || "{}");
const cid = el.dataset.containerId || null;
new TidikoAiWidgetPreact(cid, params);
}
});

Integrazione HTML

Esempio di integrazione
<!-- Integrazione automatica con data attributes -->
<div
data-tidiko-ai-widget-preact='{
"platformSettings": {
"chatTitle": "Il Mio Chatbot",
"inputPlaceholder": "Come posso aiutarti?",
"colorScheme": "dark",
"userBg": "#ff6b35",
"aiBg": "#f8f9fa"
},
"appUrl": "https://api.example.com",
"widgetAssistantId": "assistant_123",
"language": "Italian"
}'
data-container-id="my-widget-container">
</div>

<!-- Oppure integrazione manuale -->
<script src="https://example.com/widget/tidiko-widget-preact.js"></script>
<script>
const widget = new TidikoAiWidgetPreact("my-container", {
platformSettings: {
chatTitle: "Il Mio Chatbot",
inputPlaceholder: "Come posso aiutarti?",
colorScheme: "dark",
userBg: "#ff6b35",
aiBg: "#f8f9fa",
buttonColor: "#007bff",
useCustomLogo: true,
logoUrl: "https://example.com/logo.png",
faqMessages: [
"Quali sono i vostri orari?",
"Come posso contattarvi?",
"Offrite servizi di consegna?"
]
},
appUrl: "https://api.example.com",
websiteUrl: "https://example.com",
jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
socketUrl: "wss://socket.example.com",
type: "assistant",
widgetAssistantId: "assistant_123",
language: "Italian",
hasDocuments: true,
assistantInstructions: "Sei un assistente virtuale specializzato...",
companyName: "La Mia Azienda",
companyDescription: "Descrizione della mia azienda...",
collectionName: "prodotti",
baseFeedUrl: "https://api.example.com/feed",
fullHeight: false,
linksTarget: "_blank",
payload: { userId: "123", sessionId: "abc" }
});
</script>

🚀 Build e Deployment Preact

Script NPM Disponibili

package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:widget": "node scripts/build-widget.js",
"build:widget-preact": "node scripts/build-widget-preact.js",
"build:all": "npm run build && npm run build:widget && npm run build:widget-preact",
"dev:widget": "vite --config vite.widget-dev.config.js",
"dev:widget-preact": "vite --config vite.widget-preact-dev.config.js",
"watch:widget": "nodemon --watch resources/js/shadowDomWidget.js --watch resources/scss/ --ext js,scss --exec \"npm run build:widget\"",
"watch:widget-preact": "nodemon --watch resources/js/shadowDomWidgetPreact.jsx --watch resources/scss/ --ext jsx,scss --exec \"npm run build:widget-preact\""
}
}

Processo di Build Preact

scripts/build-widget-preact.js
#!/usr/bin/env node

import { build } from "vite";
import { resolve } from "path";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { scssInlinePlugin } from "../build-plugins/scss-inline-plugin.js";

const WIDGET_OUTPUT_DIR = "public/widget";
const WIDGET_OUTPUT_FILE = "tidiko-widget-preact.js";

async function buildWidgetPreact() {
console.log("🚀 Building Tidiko AI Widget Preact...\n");

try {
// Ensure output directory exists
if (!existsSync(WIDGET_OUTPUT_DIR)) {
mkdirSync(WIDGET_OUTPUT_DIR, { recursive: true });
}

// Build configuration for standalone Preact widget
const buildConfig = {
configFile: false,
build: {
lib: {
entry: resolve(process.cwd(), "resources/js/shadowDomWidgetPreact.jsx"),
name: "TidikoAiWidgetPreact",
fileName: () => WIDGET_OUTPUT_FILE,
formats: ["umd"],
},
outDir: WIDGET_OUTPUT_DIR,
emptyOutDir: false,
minify: "terser",
terserOptions: {
keep_fnames: true,
keep_classnames: true,
compress: {
drop_console: false,
},
},
rollupOptions: {
external: [],
output: {
globals: {}
},
},
},
plugins: [scssInlinePlugin()],
resolve: {
alias: {
"@": resolve(process.cwd(), "resources"),
},
},
};

await build(buildConfig);
console.log('✅ Preact Widget built successfully!');
} catch (error) {
console.error('❌ Preact Build failed:', error);
process.exit(1);
}
}

buildWidgetPreact();

Processo di Build JavaScript Vanilla (Legacy)

scripts/build-widget.js
#!/usr/bin/env node

import { build } from "vite";
import { resolve } from "path";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { scssInlinePlugin } from "../build-plugins/scss-inline-plugin.js";

const WIDGET_OUTPUT_DIR = "public/widget";
const WIDGET_OUTPUT_FILE = "tidiko-widget.js";

async function buildWidget() {
console.log("🚀 Building Tidiko AI Widget (Legacy)...\n");

try {
// Ensure output directory exists
if (!existsSync(WIDGET_OUTPUT_DIR)) {
mkdirSync(WIDGET_OUTPUT_DIR, { recursive: true });
}

// Build configuration for standalone widget
const buildConfig = {
configFile: false,
build: {
lib: {
entry: resolve(process.cwd(), "resources/js/shadowDomWidget.js"),
name: "TidikoAiWidget",
fileName: () => WIDGET_OUTPUT_FILE,
formats: ["umd"],
},
outDir: WIDGET_OUTPUT_DIR,
emptyOutDir: false,
minify: "terser",
terserOptions: {
keep_fnames: true,
keep_classnames: true,
compress: {
drop_console: false,
},
},
rollupOptions: {
external: [],
output: {
globals: {}
},
},
},
plugins: [scssInlinePlugin()],
resolve: {
alias: {
"@": resolve(process.cwd(), "resources"),
},
},
};

await build(buildConfig);
console.log('✅ Widget built successfully!');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}

buildWidget();

File di Output

I widget compilati vengono generati in:

Widget Preact (Nuovo):

  • public/widget/tidiko-widget-preact.js - File widget Preact standalone (UMD)
  • public/widget-dev/tidiko-widget-preact-dev.js - File widget Preact development (ES/UMD)
  • public/widget-dev/tidiko-widget-preact-dev.umd.js - File widget Preact development UMD

Widget JavaScript Vanilla (Legacy):

  • public/widget/tidiko-widget.js - File widget standalone (UMD)
  • public/widget/integration.html - Demo interattiva
  • public/widget-dev/tidiko-widget-dev.js - File widget development (ES/UMD)
  • public/widget-dev/tidiko-widget-dev.umd.js - File widget development UMD

Dipendenze Richieste

package.json
{
"dependencies": {
"marked": "^14.1.2",
"socket.io-client": "^4.7.5",
"preact": "^10.19.3",
"preact-compat": "^3.19.0"
},
"devDependencies": {
"sass-embedded": "^1.83.1",
"vite": "^6.3.5",
"terser": "^5.37.0",
"@preact/preset-vite": "^2.8.2"
}
}

Configurazione Vite per Preact

vite.widget-preact-dev.config.js
import { defineConfig } from "vite";
import { scssInlinePlugin } from "./build-plugins/scss-inline-plugin.js";
import * as sassEmbedded from "sass-embedded";
import preact from "@preact/preset-vite";

export default defineConfig({
root: "resources",

server: {
host: "0.0.0.0",
port: 5175,
hmr: {
host: "localhost",
protocol: "ws",
},
watch: {
usePolling: true,
},
cors: true,
},

build: {
outDir: "../public/widget-dev",
emptyOutDir: true,
lib: {
entry: "js/shadowDomWidgetPreact.jsx",
name: "TidikoAiWidgetPreact",
fileName: "tidiko-widget-preact-dev",
formats: ["es", "umd"],
},
rollupOptions: {
external: [],
output: {
globals: {}
}
},
sourcemap: true,
minify: false,
},

plugins: [
preact(),
scssInlinePlugin()
],

css: {
devSourcemap: true,
preprocessorOptions: {
scss: {
implementation: sassEmbedded,
},
},
},

define: {
"import.meta.env.DEV": true,
},
});

Configurazione Vite JavaScript Vanilla (Legacy)

vite.widget-dev.config.js
import { defineConfig } from "vite";
import { scssInlinePlugin } from "./build-plugins/scss-inline-plugin.js";
import * as sassEmbedded from "sass-embedded";

export default defineConfig({
root: "resources",

server: {
host: "0.0.0.0",
port: 5174,
hmr: {
host: "localhost",
protocol: "ws",
},
watch: {
usePolling: true,
},
cors: true,
},

build: {
outDir: "../public/widget-dev",
emptyOutDir: true,
lib: {
entry: "js/shadowDomWidget.js",
name: "TidikoAiWidget",
fileName: "tidiko-widget-dev",
formats: ["es", "umd"],
},
rollupOptions: {
external: [],
output: {
globals: {}
}
},
sourcemap: true,
minify: false,
},

plugins: [scssInlinePlugin()],

css: {
devSourcemap: true,
preprocessorOptions: {
scss: {
implementation: sassEmbedded,
},
},
},

define: {
"import.meta.env.DEV": true,
},
});

📱 Responsive Design

Breakpoints

resources/scss/agentWidget.scss
$breakpoints: (
"xs": 370px,
"sm": 480px,
"md": 768px,
"lg": 1024px,
"xl": 1200px,
);

@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
}
}

.chat-container {
width: 350px;
height: 500px;

@include respond-to(sm) {
width: 400px;
height: 600px;
}

@include respond-to(md) {
width: 450px;
height: 700px;
}
}

🔒 Sicurezza

JWT Authentication

resources/js/shadowDomWidget.js
_setupAuthentication() {
const ls = window.localStorage;
if (this.jwt) ls.setItem("tidiko_jwt", this.jwt);
if (this.jwtRefresh) ls.setItem("tidiko_jwt_refresh", this.jwtRefresh);
}

// Validazione token
_validateToken() {
const token = window.localStorage.getItem("tidiko_jwt");
if (!token) return false;

try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp > Date.now() / 1000;
} catch (e) {
return false;
}
}

Privacy e GDPR

resources/js/shadowDomWidget.js
_showPrivacyBanner() {
const msg = document.createElement("div");
msg.className = "message system-message privacy-message";
msg.innerHTML = `
<p>${this.options.translations?.privacy?.message ||
"By using Tidiko AI Chat, you accept our"}
<a href="https://tidiko.ai/privacy-policy-app-tidiko/" target="_blank">
${this.options.translations?.privacy?.policyLink || "Privacy Policy"}
</a>.
</p>
<button id="accept-privacy" class="privacy-button">
${this.options.translations?.privacy?.acceptButton || "OK"}
</button>
`;

this.dom.chatHistory.appendChild(msg);
this.dom.messageInput.disabled = true;
this.dom.messageSend.disabled = true;

const btn = this.shadowRoot.getElementById("accept-privacy");
btn.addEventListener("click", async () => {
msg.remove();
this.dom.messageInput.disabled = false;
this.dom.messageSend.disabled = !this.dom.messageInput.value.trim();
this._initFAQ();

const exp = new Date();
exp.setFullYear(exp.getFullYear() + 1);
document.cookie = `tidiko_widget_privacy_accepted=1;expires=${exp.toUTCString()};path=/`;
});
}

⚡ Caratteristiche Specifiche Preact

Vantaggi del Widget Preact

Il nuovo widget Preact offre diversi vantaggi rispetto alla versione JavaScript vanilla:

🚀 Performance Ottimizzate

resources/js/shadowDomWidgetPreact.jsx
// Componenti ottimizzati con memo() per evitare re-render inutili
const ProductCard = memo(({ id, url, addToCartUrl, ... }) => {
// Componente ottimizzato che si ri-renderizza solo quando le props cambiano
});

// Hooks ottimizzati con useCallback per evitare ricreazioni di funzioni
const handleSubmit = useCallback(async (e) => {
// Funzione ottimizzata che mantiene la stessa referenza tra i render
}, [currentMessage, isRunning, sendMessage]);

// Valori memoizzati con useMemo per calcoli costosi
const wrapperStyle = useMemo(() => {
const style = {};
// Calcoli complessi per gli stili CSS
return style;
}, [options, fullHeight, getContrastColor, hexToRGB, getShimmerColors]);

🧩 Architettura Modulare

resources/js/shadowDomWidgetPreact.jsx
// Separazione chiara delle responsabilità
const TidikoWidget = ({ options, payload, ... }) => {
// State management centralizzato
const [currentThreadId, setCurrentThreadId] = useState(null);
const [isRunning, setIsRunning] = useState(false);
const [messages, setMessages] = useState([]);

// Effetti separati per diverse responsabilità
useEffect(() => {
// Socket setup
}, [socketUrl, assistantTextClass, userTextClass]);

useEffect(() => {
// Authentication setup
}, [jwt, jwtRefresh]);

useEffect(() => {
// Thread loading
}, [widgetAssistantId]);

// Componenti composabili
return (
<div className={wrapperClass} style={wrapperStyle}>
<ChatHeader {...headerProps} />
<ChatHistory {...historyProps} />
<MessageForm {...formProps} />
</div>
);
};

🎨 Gestione Stili Dinamici

resources/js/shadowDomWidgetPreact.jsx
// CSS Variables dinamiche con useMemo
const wrapperStyle = useMemo(() => {
const style = {};

if (options.userBg) {
style["--user-message-bg"] = options.userBg;
style["--user-message-color"] = getContrastColor(options.userBg);
const rgb = Object.values(hexToRGB(options.userBg)).join(" ");
style["--faq-bg-color"] = `rgb(${rgb}/1)`;
style["--faq-bg-color-hover"] = `rgb(${rgb}/.7)`;
}

if (options.aiBg) {
style["--assistant-message-bg"] = options.aiBg;
const assistantTextColor = getContrastColor(options.aiBg);
style["--assistant-message-color"] = assistantTextColor;

// Set shimmer colors based on assistant text color
const shimmerColors = getShimmerColors(
assistantTextColor,
options.aiBg
);
style["--shimmer-accent-color"] = shimmerColors.accentColor;
style["--shimmer-main-color"] = shimmerColors.mainColor;
}

if (options.buttonColor) {
style["--button-bg"] = options.buttonColor;
style["--button-color"] = getContrastColor(options.buttonColor);
}

if (fullHeight) {
style["--widget-max-height"] = "100%";
}

return style;
}, [options, fullHeight, getContrastColor, hexToRGB, getShimmerColors]);

🔄 Gestione Stato Reattiva

resources/js/shadowDomWidgetPreact.jsx
// State management reattivo con hooks
const [currentThreadId, setCurrentThreadId] = useState(null);
const [isRunning, setIsRunning] = useState(false);
const [tmpMsg, setTmpMsg] = useState("");
const [tmpMsgType, setTmpMsgType] = useState("");
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
const [messages, setMessages] = useState([]);
const [currentMessage, setCurrentMessage] = useState("");
const [showTools, setShowTools] = useState(false);
const [copyMessage, setCopyMessage] = useState("");
const [showPrivacy, setShowPrivacy] = useState(false);
const [showFAQ, setShowFAQ] = useState(false);
const [inputDisabled, setInputDisabled] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [isSocketConnected, setIsSocketConnected] = useState(false);
const [isHistoryLoading, setIsHistoryLoading] = useState(true);
const [loadedMessages, setLoadedMessages] = useState([]);

🎯 Gestione Eventi Ottimizzata

resources/js/shadowDomWidgetPreact.jsx
// Event handlers ottimizzati con useCallback
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (!currentMessage.trim() || isRunning) return;

const text = currentMessage.trim();
setCurrentMessage("");
await sendMessage(text);
}, [currentMessage, isRunning, sendMessage]);

const handleKeyDown = useCallback((e) => {
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
if (!inputDisabled && !isRunning && currentMessage.trim()) {
handleSubmit(e);
}
}
}, [handleSubmit, inputDisabled, isRunning, currentMessage]);

const handleFAQQuestionClick = useCallback(async (question) => {
if (isRunning || inputDisabled || !question.trim()) return;

setShowFAQ(false);
await sendMessage(question.trim());
}, [isRunning, inputDisabled, sendMessage]);

🔧 Utility Functions Avanzate

resources/js/shadowDomWidgetPreact.jsx
// Funzioni di utilità per la gestione dei colori
const hexToRGB = useCallback((hex) => ({
r: parseInt(hex.slice(1, 3), 16),
g: parseInt(hex.slice(3, 5), 16),
b: parseInt(hex.slice(5, 7), 16),
}), []);

const rgbToHSL = useCallback((r, g, b) => {
// Normalize RGB values to 0-1 range
r /= 255;
g /= 255;
b /= 255;

const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l;

// Calculate lightness
l = (max + min) / 2;

if (max === min) {
// Achromatic (gray)
h = s = 0;
} else {
const d = max - min;
// Calculate saturation
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
// Calculate hue
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
h = 0;
}
h /= 6;
}

return {
h: Math.round(h * 360), // Convert to degrees
s: Math.round(s * 100), // Convert to percentage
l: Math.round(l * 100), // Convert to percentage
};
}, []);

// Funzione per calcolare il contrasto dei colori
const checkAPCAContrast = useCallback((bg, tx) => {
const rgbBg = hexToRGB(bg);
const rgbTx = hexToRGB(tx);
const bgY =
0.2126 * sRGBtoY(rgbBg.r) +
0.7152 * sRGBtoY(rgbBg.g) +
0.0722 * sRGBtoY(rgbBg.b);
const txY =
0.2126 * sRGBtoY(rgbTx.r) +
0.7152 * sRGBtoY(rgbTx.g) +
0.0722 * sRGBtoY(rgbTx.b);
const contrast = Math.abs(
(bgY > txY
? bgY ** 0.56 - txY ** 0.57
: txY ** 0.56 - bgY ** 0.57) * 1.14
);
return contrast * 100;
}, [hexToRGB, sRGBtoY]);

📦 Estrazione Product Cards

resources/js/shadowDomWidgetPreact.jsx
// Funzione avanzata per l'estrazione delle product cards
const extractProductCardsData = (content) => {
// First decode HTML entities
const decodedContent = content
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");

const productCardsData = [];
let processedBuilder = "";
let cursor = 0;
const lowerContent = decodedContent.toLowerCase();

// Greedy scan: detect even partial opening tags (case-insensitive)
while (true) {
// Find earliest occurrence among patterns to also catch partial tokens like "<prod"
const patterns = ["<product-card", "<product", "<prod"];
let startIdx = -1;
for (let i = 0; i < patterns.length; i++) {
const idx = lowerContent.indexOf(patterns[i], cursor);
if (idx !== -1 && (startIdx === -1 || idx < startIdx)) {
startIdx = idx;
}
}
if (startIdx === -1) break;

// Process product card data...
// (continua con la logica di estrazione)
}

return {
content: processedBuilder.trim(),
productCards: productCardsData,
};
};

Il sistema di widget Preact di Tidiko AI fornisce un'integrazione moderna, performante e completamente isolata per chatbot intelligenti, con supporto per personalizzazione avanzata, comunicazione real-time, gestione stato reattiva e conformità GDPR. L'architettura a componenti Preact garantisce maggiore manutenibilità, performance ottimizzate e un'esperienza di sviluppo moderna.