Passa al contenuto principale

Eventi e Socket

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,
]);