hotel-ai-widget
Version:
A customizable hotel chat widget for React and vanilla HTML
129 lines (128 loc) • 5.46 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from "react";
export function hasMarkdownContent(text) {
const cleanText = text.replace(/\\\\n/g, "\n").replace(/\\n/g, "\n");
const markdownPatterns = [
/^#{1,6}\s+/m,
/\*\*[^*]+\*\*/,
/\*[^*]+\*/,
/`[^`]+`/,
/```[\s\S]*?```/,
/^\s*[-*+]\s+/m,
/^\s*\d+\.\s+/m,
/^\s*\|.*\|.*$/m,
/^\s*>\s+/m,
/\[([^\]]+)\]\(([^)]+)\)/,
/!\[([^\]]*)\]\(([^)]+)\)/,
/^\s*---+\s*$/m,
/\n\s*\n/,
];
return markdownPatterns.some((pattern) => pattern.test(cleanText));
}
export const processMarkdownChildren = (children, places, setHoveredMarker) => {
if (typeof children === "string") {
return renderTextWithPlaceLinks(children, places, setHoveredMarker);
}
if (Array.isArray(children)) {
return children.map((child, index) => {
if (typeof child === "string") {
return (_jsx(React.Fragment, { children: renderTextWithPlaceLinks(child, places, setHoveredMarker) }, index));
}
return child;
});
}
return children;
};
const renderTextWithPlaceLinks = (text, places, setHoveredMarker) => {
if (!places || places.length === 0) {
return text;
}
// Sort places by name length (longest first) to handle overlapping names correctly
const sortedPlaces = [...places].sort((a, b) => b.name.length - a.name.length);
// Find all place mentions in the text
const placeMatches = [];
sortedPlaces.forEach((place) => {
// Create multiple regex patterns to catch different variations
const patterns = [
// Exact match with word boundaries
new RegExp(`\\b${place.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi"),
// Match without strict word boundaries (for places with special characters)
new RegExp(`${place.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "gi"),
// Match with flexible spacing/punctuation
new RegExp(`${place.name
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
.replace(/\s+/g, "\\s*")}`, "gi"),
];
patterns.forEach((regex) => {
let match;
// Reset regex lastIndex for each pattern
regex.lastIndex = 0;
while ((match = regex.exec(text)) !== null) {
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
// Check if this match overlaps with existing matches
const overlaps = placeMatches.some((existing) => {
return ((matchStart >= existing.start && matchStart < existing.end) ||
(matchEnd > existing.start && matchEnd <= existing.end) ||
(matchStart <= existing.start && matchEnd >= existing.end));
});
if (!overlaps) {
placeMatches.push({
start: matchStart,
end: matchEnd,
place: place,
matchedText: match[0],
});
}
// Prevent infinite loop
if (regex.lastIndex === match.index) {
regex.lastIndex++;
}
}
});
});
// Sort matches by start position
placeMatches.sort((a, b) => a.start - b.start);
// Remove overlapping matches, keeping the longest ones
const filteredMatches = [];
for (const match of placeMatches) {
const hasOverlap = filteredMatches.some((existing) => {
return ((match.start >= existing.start && match.start < existing.end) ||
(match.end > existing.start && existing.end) ||
(match.start <= existing.start && match.end >= existing.end));
});
if (!hasOverlap) {
filteredMatches.push(match);
}
}
// If no matches found, return original text
if (filteredMatches.length === 0) {
return text;
}
// Build the final JSX with clickable place links
const parts = [];
let lastIndex = 0;
filteredMatches.forEach((match, index) => {
// Add text before this match
if (match.start > lastIndex) {
parts.push(text.slice(lastIndex, match.start));
}
// Add clickable place link
parts.push(_jsx("span", { className: "text-blue-600 underline cursor-pointer hover:text-blue-800 font-medium", title: match.place.description || match.place.name, onMouseEnter: () => setHoveredMarker({
id: index,
label: match?.place?.name,
lat: match?.place?.latitude,
lng: match?.place?.longitude,
}), onMouseLeave: () => setHoveredMarker(null), onClick: () => {
console.log("match", match);
alert(`📍 ${match.place.name}\n${match.place.description || "No description available"}`);
}, children: match.matchedText }, `place-link-${index}-${match.start}`));
lastIndex = match.end;
});
// Add remaining text after last match
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return _jsx(_Fragment, { children: parts });
};