UNPKG

hotel-ai-widget

Version:

A customizable hotel chat widget for React and vanilla HTML

129 lines (128 loc) 5.46 kB
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 }); };