fix: Properly decode HTML entities in chat messages
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Has been cancelled

- Fix ' not being decoded to apostrophe
- Decode HTML entities when loading messages from localStorage
- Improve server-side HTML entity decoding to handle all variations
- Replace hardcoded ' in static text with regular apostrophes
- Add support for more HTML entity variations (rsquo, lsquo, etc.)
This commit is contained in:
2026-01-09 18:07:32 +01:00
parent 9c24fdf5bd
commit 42a586d183
2 changed files with 34 additions and 12 deletions

View File

@@ -51,6 +51,14 @@ export default function ChatWidget() {
} }
}, [isOpen]); }, [isOpen]);
// Helper function to decode HTML entities
const decodeHtmlEntities = (text: string): string => {
if (!text || typeof text !== 'string') return text;
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
};
// Load messages from localStorage // Load messages from localStorage
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -61,6 +69,7 @@ export default function ChatWidget() {
setMessages( setMessages(
parsed.map((m: Message) => ({ parsed.map((m: Message) => ({
...m, ...m,
text: decodeHtmlEntities(m.text), // Decode HTML entities when loading
timestamp: new Date(m.timestamp), timestamp: new Date(m.timestamp),
})), })),
); );
@@ -72,7 +81,7 @@ export default function ChatWidget() {
setMessages([ setMessages([
{ {
id: "welcome", id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀", text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot", sender: "bot",
timestamp: new Date(), timestamp: new Date(),
}, },
@@ -128,12 +137,8 @@ export default function ChatWidget() {
// Decode HTML entities in the reply // Decode HTML entities in the reply
let replyText = data.reply || "Sorry, I couldn't process that. Please try again."; let replyText = data.reply || "Sorry, I couldn't process that. Please try again.";
// Decode HTML entities client-side as well (double safety) // Decode HTML entities client-side (double safety)
if (typeof window !== 'undefined') { replyText = decodeHtmlEntities(replyText);
const textarea = document.createElement('textarea');
textarea.innerHTML = replyText;
replyText = textarea.value;
}
const botMessage: Message = { const botMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
@@ -237,7 +242,7 @@ export default function ChatWidget() {
</div> </div>
<div> <div>
<h3 className="font-bold text-sm"> <h3 className="font-bold text-sm">
Dennis&apos;s AI Assistant Dennis{'\''}s AI Assistant
</h3> </h3>
<p className="text-xs text-white/80">Always online</p> <p className="text-xs text-white/80">Always online</p>
</div> </div>
@@ -370,7 +375,7 @@ export default function ChatWidget() {
{/* Quick Actions */} {/* Quick Actions */}
<div className="flex gap-2 mt-2 overflow-x-auto pb-1 scrollbar-hide"> <div className="flex gap-2 mt-2 overflow-x-auto pb-1 scrollbar-hide">
{[ {[
"What are Dennis&apos;s skills?", "What are Dennis's skills?",
"Tell me about his projects", "Tell me about his projects",
"How can I contact him?", "How can I contact him?",
].map((suggestion, index) => ( ].map((suggestion, index) => (

View File

@@ -21,7 +21,7 @@ export function decodeHtmlEntitiesServer(text: string): string {
return text; return text;
} }
// Map of common HTML entities // Map of common HTML entities (including all variations of apostrophe)
const entityMap: Record<string, string> = { const entityMap: Record<string, string> = {
'&apos;': "'", '&apos;': "'",
'&quot;': '"', '&quot;': '"',
@@ -33,9 +33,26 @@ export function decodeHtmlEntitiesServer(text: string): string {
'&#x2F;': '/', '&#x2F;': '/',
'&#x60;': '`', '&#x60;': '`',
'&#x3D;': '=', '&#x3D;': '=',
'&rsquo;': "'",
'&lsquo;': "'",
'&rdquo;': '"',
'&ldquo;': '"',
}; };
return text.replace(/&[#\w]+;/g, (entity) => { // First replace known entities
return entityMap[entity] || entity; let decoded = text;
for (const [entity, replacement] of Object.entries(entityMap)) {
decoded = decoded.replace(new RegExp(entity, 'gi'), replacement);
}
// Then handle numeric entities (&#39; &#x27; etc.)
decoded = decoded.replace(/&#(\d+);/g, (match, num) => {
return String.fromCharCode(parseInt(num, 10));
}); });
decoded = decoded.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
return String.fromCharCode(parseInt(hex, 16));
});
return decoded;
} }