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:
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:
// 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:
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:
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:
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:
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:
.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
_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:
// 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:
// 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:
// 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
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
_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
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
// 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
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
<!-- 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
{
"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
#!/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)
#!/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 interattivapublic/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
{
"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
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)
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
$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
_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
_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
// 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
// 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
// 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
// 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
// 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
// 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
// Funzione avanzata per l'estrazione delle product cards
const extractProductCardsData = (content) => {
// First decode HTML entities
const decodedContent = content
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/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.