UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

631 lines (628 loc) 26.2 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useRef, useMemo, useCallback, useEffect } from 'react'; import { GlassCore } from '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useAccessibility } from '../../hooks/useAccessibility.js'; // NLP Query Analysis const analyzeQuery = query => { const lowercaseQuery = query.toLowerCase(); // Intent detection let intent = "search"; if (lowercaseQuery.includes("find") || lowercaseQuery.includes("show") || lowercaseQuery.includes("get")) { intent = "retrieve"; } else if (lowercaseQuery.includes("compare") || lowercaseQuery.includes("vs") || lowercaseQuery.includes("versus")) { intent = "compare"; } else if (lowercaseQuery.includes("best") || lowercaseQuery.includes("top") || lowercaseQuery.includes("recommend")) { intent = "recommend"; } else if (lowercaseQuery.includes("help") || lowercaseQuery.includes("how to") || lowercaseQuery.includes("tutorial")) { intent = "help"; } // Entity extraction (simplified) const entities = []; // Date entities const dateMatches = query.match(/(\d{4}|\d{1,2}\/\d{1,2}\/\d{4}|today|yesterday|last week|this month)/gi); dateMatches?.forEach(match => { entities.push({ type: "date", value: match }); }); // Number entities const numberMatches = query.match(/\b\d+(?:\.\d+)?\b/g); numberMatches?.forEach(match => { entities.push({ type: "number", value: match }); }); // Category entities (simplified) const categories = ["product", "service", "article", "document", "image", "video", "user", "project"]; categories.forEach(category => { if (lowercaseQuery.includes(category)) { entities.push({ type: "category", value: category }); } }); // Sentiment analysis (simplified) const positiveWords = ["good", "great", "excellent", "amazing", "best", "love", "fantastic"]; const negativeWords = ["bad", "terrible", "awful", "worst", "hate", "horrible", "useless"]; const hasPositive = positiveWords.some(word => lowercaseQuery.includes(word)); const hasNegative = negativeWords.some(word => lowercaseQuery.includes(word)); let sentiment = "neutral"; if (hasPositive && !hasNegative) sentiment = "positive";else if (hasNegative && !hasPositive) sentiment = "negative"; // Extract keywords (remove stop words) const stopWords = ["the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "is", "are", "was", "were", "been", "be", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", "this", "that", "these", "those"]; const keywords = query.toLowerCase().split(/\s+/).filter(word => word.length > 2 && !stopWords.includes(word)).filter((word, index, arr) => arr.indexOf(word) === index); // Remove duplicates return { intent, entities, sentiment, keywords }; }; // Advanced search algorithm with NLP const performIntelligentSearch = (query, data, filters, enableNLP = true) => { if (!query.trim() && Object.keys(filters).length === 0) { return data; } const analysis = enableNLP ? analyzeQuery(query) : null; const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0); return data.map(item => { let score = 0; // Text matching with different weights const titleMatch = searchTerms.reduce((acc, term) => { return acc + (item.title.toLowerCase().includes(term) ? 1 : 0); }, 0); const descriptionMatch = searchTerms.reduce((acc, term) => { return acc + (item.description.toLowerCase().includes(term) ? 0.5 : 0); }, 0); const tagMatch = searchTerms.reduce((acc, term) => { return acc + item.tags.filter(tag => tag.toLowerCase().includes(term)).length * 0.7; }, 0); score = titleMatch * 3 + descriptionMatch + tagMatch; // NLP-based scoring boost if (analysis && enableNLP) { // Intent-based scoring if (analysis.intent === "recommend" && item.metadata?.rating) { score += item.metadata.rating * 0.5; } // Entity matching analysis.entities.forEach(entity => { if (entity.type === "category" && item.category.toLowerCase().includes(entity.value)) { score += 1; } }); // Keyword density bonus const keywordMatches = analysis.keywords.filter(keyword => item.title.toLowerCase().includes(keyword) || item.description.toLowerCase().includes(keyword)).length; score += keywordMatches * 0.3; } // Filter matching let passesFilters = true; Object.entries(filters).forEach(([key, value]) => { if (!value || Array.isArray(value) && value.length === 0) return; switch (key) { case "category": if (Array.isArray(value)) { passesFilters = passesFilters && value.includes(item.category); } else { passesFilters = passesFilters && item.category === value; } break; case "tags": if (Array.isArray(value)) { passesFilters = passesFilters && value.some(tag => item.tags.includes(tag)); } break; case "dateRange": if (item.metadata?.date) { const itemDate = new Date(item.metadata.date); const startDate = new Date(value.start); const endDate = new Date(value.end); passesFilters = passesFilters && itemDate >= startDate && itemDate <= endDate; } break; case "rating": if (item.metadata?.rating) { passesFilters = passesFilters && item.metadata.rating >= value; } break; } }); if (!passesFilters) { score = 0; } // Generate highlights const highlights = {}; if (score > 0) { searchTerms.forEach(term => { if (item.title.toLowerCase().includes(term)) { if (!highlights.title) highlights.title = []; highlights.title.push(term); } if (item.description.toLowerCase().includes(term)) { if (!highlights.description) highlights.description = []; highlights.description.push(term); } }); } return { ...item, score, highlights }; }).filter(item => item.score > 0).sort((a, b) => b.score - a.score); }; // Generate search suggestions const generateSuggestions = (query, data, recentSearches) => { const suggestions = []; const lowercaseQuery = query.toLowerCase(); // Recent searches recentSearches.filter(search => search.toLowerCase().includes(lowercaseQuery)).slice(0, 3).forEach(search => { suggestions.push({ text: search, type: "query" }); }); // Category suggestions const categories = [...new Set(data.map(item => item.category))]; categories.filter(category => category.toLowerCase().includes(lowercaseQuery)).slice(0, 3).forEach(category => { const count = data.filter(item => item.category === category).length; suggestions.push({ text: category, type: "category", category: "Categories", count }); }); // Tag suggestions const allTags = data.flatMap(item => item.tags); const tagCounts = allTags.reduce((acc, tag) => { acc[tag] = (acc[tag] || 0) + 1; return acc; }, {}); Object.entries(tagCounts).filter(([tag]) => tag.toLowerCase().includes(lowercaseQuery)).sort(([, a], [, b]) => b - a).slice(0, 5).forEach(([tag, count]) => { suggestions.push({ text: tag, type: "tag", category: "Tags", count }); }); return suggestions; }; const GlassIntelligentSearch = ({ data, onSearch, onResultClick, placeholder = "Search with natural language...", showFilters = true, showSuggestions = true, enableNLP = true, enableVoiceSearch = false, maxResults = 50, className }) => { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [suggestions, setSuggestions] = useState([]); const [filters, setFilters] = useState({}); const [showSuggestionsList, setShowSuggestionsList] = useState(false); const [isSearching, setIsSearching] = useState(false); const [recentSearches, setRecentSearches] = useState([]); const [searchAnalysis, setSearchAnalysis] = useState(null); const [isListening, setIsListening] = useState(false); const searchInputRef = useRef(null); const searchTimeoutRef = useRef(); const suggestionRefs = useRef([]); useAccessibility(); // Generate available filters from data const availableFilters = useMemo(() => { const categories = [...new Set(data.map(item => item.category))]; const allTags = data.flatMap(item => item.tags); const tagCounts = allTags.reduce((acc, tag) => { acc[tag] = (acc[tag] || 0) + 1; return acc; }, {}); const topTags = Object.entries(tagCounts).sort(([, a], [, b]) => b - a).slice(0, 20).map(([tag, count]) => ({ value: tag, label: tag, count })); return [{ id: "category", name: "Category", type: "multiselect", options: categories.map(cat => ({ value: cat, label: cat, count: data.filter(item => item.category === cat).length })) }, { id: "tags", name: "Tags", type: "multiselect", options: topTags }, { id: "rating", name: "Minimum Rating", type: "range", range: { min: 0, max: 5, step: 0.5 } }]; }, [data]); // Debounced search const performSearch = useCallback((searchQuery, currentFilters) => { setIsSearching(true); clearTimeout(searchTimeoutRef.current); searchTimeoutRef.current = setTimeout(() => { const searchResults = performIntelligentSearch(searchQuery, data, currentFilters, enableNLP); const limitedResults = searchResults.slice(0, maxResults); setResults(limitedResults); setIsSearching(false); if (enableNLP && searchQuery.trim()) { setSearchAnalysis(analyzeQuery(searchQuery)); } onSearch?.(searchQuery, currentFilters); }, 300); }, [data, enableNLP, maxResults, onSearch]); // Update suggestions when query changes useEffect(() => { if (query.length > 0) { const newSuggestions = generateSuggestions(query, data, recentSearches); setSuggestions(newSuggestions); setShowSuggestionsList(true); } else { setSuggestions([]); setShowSuggestionsList(false); } }, [query, data, recentSearches]); // Perform search when query or filters change useEffect(() => { performSearch(query, filters); }, [query, filters, performSearch]); const handleQueryChange = value => { setQuery(value); }; const handleSuggestionClick = suggestion => { if (suggestion.type === "query") { setQuery(suggestion.text); } else if (suggestion.type === "category") { setFilters(prev => ({ ...prev, category: [...(prev.category || []), suggestion.text] })); } else if (suggestion.type === "tag") { setFilters(prev => ({ ...prev, tags: [...(prev.tags || []), suggestion.text] })); } setShowSuggestionsList(false); searchInputRef.current?.focus(); }; const handleSearchSubmit = () => { if (query.trim() && !recentSearches.includes(query.trim())) { setRecentSearches(prev => [query.trim(), ...prev.slice(0, 9)]); } setShowSuggestionsList(false); }; const handleFilterChange = (filterId, value) => { setFilters(prev => ({ ...prev, [filterId]: value })); }; const clearFilters = () => { setFilters({}); }; const startVoiceSearch = () => { if (!enableVoiceSearch || !("webkitSpeechRecognition" in window)) return; const recognition = new window.webkitSpeechRecognition(); recognition.lang = "en-US"; recognition.continuous = false; recognition.interimResults = false; recognition.onstart = () => { setIsListening(true); }; recognition.onresult = event => { const transcript = event.results[0][0].transcript; setQuery(transcript); setIsListening(false); }; recognition.onerror = () => { setIsListening(false); }; recognition.onend = () => { setIsListening(false); }; recognition.start(); }; const highlightText = (text, highlights = []) => { if (!highlights.length) return text; let highlightedText = text; highlights.forEach(term => { const regex = new RegExp(`(${term})`, "gi"); highlightedText = highlightedText.replace(regex, '<mark class="bg-yellow-200 px-1 rounded">$1</mark>'); }); return jsx("span", { dangerouslySetInnerHTML: { __html: highlightedText } }); }; const getResultIcon = category => { const icons = { document: "📄", article: "📝", image: "🖼️", video: "🎥", product: "🛍️", user: "👤", project: "📁", service: "⚙️" }; return icons[category.toLowerCase()] || "🔍"; }; return jsxs("div", { "data-glass-component": true, className: cn("w-full max-w-4xl mx-auto", className), children: [jsxs(GlassCore, { className: 'relative', children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-3 glass-p-4", children: [jsxs("div", { className: 'relative glass-flex-1', children: [jsx("input", { ref: searchInputRef, type: "text", value: query, onChange: e => handleQueryChange(e.target.value), onKeyDown: e => { if (e.key === "Enter") { handleSearchSubmit(); } else if (e.key === "Escape") { setShowSuggestionsList(false); } }, onFocus: () => setShowSuggestionsList(suggestions.length > 0), placeholder: placeholder, className: 'glass-w-full pl-10 pr-4 glass-py-3 glass-border glass-border-subtle glass-radius-lg focus:ring-2 focus:ring-blue-500 focus:border-blue glass-focus glass-touch-target glass-contrast-guard', "aria-label": "Search input" }), jsx("div", { className: 'absolute left-3 glass-top-1/2 transform -translate-y-1/2', children: isSearching ? jsx("div", { className: 'animate-spin w-5 h-5 glass-border-2 glass-border-blue glass-border-t-transparent glass-radius-full' }) : jsx("span", { className: "glass-text-secondary glass-text-lg", children: "\uD83D\uDD0D" }) })] }), enableVoiceSearch && jsx("button", { onClick: startVoiceSearch, disabled: isListening, className: cn("px-3 py-3 rounded-lg transition-colors glass-focus glass-touch-target glass-contrast-guard", isListening ? "bg-red-100 text-red-600 glass-animate-pulse" : "bg-gray-100 text-gray-600 hover:bg-gray-200"), title: "Voice search", children: "\uD83C\uDFA4" }), jsx("button", { onClick: handleSearchSubmit, className: 'glass-px-6 glass-py-3 glass-surface-blue text-primary glass-radius-lg hover:glass-surface-blue transition-colors glass-focus glass-touch-target glass-contrast-guard', children: "Search" })] }), showSuggestionsList && suggestions.length > 0 && jsx("div", { className: 'absolute top-full left-0 right-0 mt-2 glass-surface-subtle glass-border glass-border-subtle glass-radius-lg glass-shadow-lg z-50 max-h-60 overflow-y-auto', children: suggestions.map((suggestion, index) => jsxs("button", { ref: el => suggestionRefs.current[index] = el, onClick: () => handleSuggestionClick(suggestion), className: 'glass-w-full glass-px-4 glass-py-2 text-left hover:glass-surface-subtle glass-flex glass-items-center glass-justify-between glass-border-b glass-border-gray-100 last:border-b-0 glass-focus glass-touch-target glass-contrast-guard', children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-3", children: [jsxs("span", { className: "glass-text-sm", children: [suggestion.type === "query" && "🔍", suggestion.type === "category" && "📁", suggestion.type === "tag" && "🏷️"] }), jsx("span", { className: "glass-text-secondary", children: suggestion.text }), suggestion.category && jsxs("span", { className: "glass-text-xs glass-text-secondary", children: ["in ", suggestion.category] })] }), suggestion.count && jsx("span", { className: "glass-text-xs glass-text-secondary", children: suggestion.count })] }, `${suggestion.type}-${suggestion.text}`)) })] }), enableNLP && searchAnalysis && query.trim() && jsx(GlassCore, { className: 'mt-4 glass-p-4 glass-surface-subtle', children: jsxs("div", { className: "glass-flex glass-items-center glass-gap-4 glass-text-sm", children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: 'font-medium text-primary', children: "Intent:" }), jsx("span", { className: 'glass-px-2 glass-py-1 glass-surface-subtle text-primary glass-radius capitalize', children: searchAnalysis.intent })] }), searchAnalysis.entities.length > 0 && jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: 'font-medium text-primary', children: "Entities:" }), jsx("div", { className: "glass-flex glass-gap-1", children: searchAnalysis.entities.slice(0, 3).map((entity, index) => jsxs("span", { className: 'glass-px-2 glass-py-1 glass-surface-subtle text-primary glass-radius glass-text-xs', children: [entity.type, ": ", entity.value] }, index)) })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: 'font-medium text-primary', children: "Keywords:" }), jsx("div", { className: "glass-flex glass-gap-1", children: searchAnalysis.keywords.slice(0, 4).map((keyword, index) => jsx("span", { className: 'glass-px-2 glass-py-1 glass-surface-subtle text-primary glass-radius glass-text-xs', children: keyword }, index)) })] })] }) }), showFilters && availableFilters.length > 0 && jsxs(GlassCore, { className: 'mt-4 glass-p-4', children: [jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-4', children: [jsx("h3", { className: 'glass-text-lg font-semibold glass-text-secondary', children: "Filters" }), Object.keys(filters).length > 0 && jsx("button", { onClick: clearFilters, className: 'glass-text-sm text-primary hover:text-primary glass-focus glass-touch-target glass-contrast-guard', children: "Clear all" })] }), jsx("div", { className: 'glass-grid glass-grid-cols-1 md:glass-grid-cols-3 glass-gap-4', children: availableFilters.map(filter => jsxs("div", { children: [jsx("label", { className: 'block glass-text-sm font-medium glass-text-secondary mb-2', children: filter.name }), filter.type === "multiselect" && jsx("div", { className: 'space-y-2 max-h-32 overflow-y-auto', children: filter.options?.map(option => jsxs("label", { className: "glass-flex glass-items-center glass-gap-2 glass-text-sm", children: [jsx("input", { type: "checkbox", checked: filters[filter.id]?.includes(option.value) || false, onChange: e => { const current = filters[filter.id] || []; if (e.target.checked) { handleFilterChange(filter.id, [...current, option.value]); } else { handleFilterChange(filter.id, current.filter(v => v !== option.value)); } }, className: 'glass-radius glass-border-subtle text-primary focus:ring-blue-500 glass-focus glass-touch-target glass-contrast-guard' }), jsx("span", { className: "glass-flex-1", children: option.label }), jsxs("span", { className: "glass-text-secondary glass-text-xs", children: ["(", option.count, ")"] })] }, option.value)) }), filter.type === "range" && filter.range && jsxs("div", { children: [jsx("input", { type: "range", min: filter.range.min, max: filter.range.max, step: filter.range.step || 1, value: filters[filter.id] || filter.range.min, onChange: e => handleFilterChange(filter.id, parseFloat(e.target.value)), className: "glass-w-full glass-focus glass-touch-target glass-contrast-guard" }), jsxs("div", { className: 'glass-flex glass-justify-between glass-text-xs glass-text-secondary mt-1', children: [jsx("span", { children: filter.range.min }), jsx("span", { className: 'font-medium', children: filters[filter.id] || filter.range.min }), jsx("span", { children: filter.range.max })] })] })] }, filter.id)) })] }), jsxs("div", { className: 'mt-6', children: [query.trim() || Object.keys(filters).length > 0 ? jsxs("div", { className: 'mb-4 glass-text-sm glass-text-secondary', children: ["Found ", results.length, " results", query.trim() && jsxs("span", { children: [" ", "for \"", jsx("span", { className: 'font-medium', children: query }), "\""] }), Object.keys(filters).length > 0 && jsxs("span", { children: [" with ", Object.keys(filters).length, " filters applied"] })] }) : null, jsxs("div", { className: 'space-y-4', children: [results.map(result => jsx(GlassCore, { className: 'glass-p-6 cursor-pointer hover:shadow-lg transition-shadow', onClick: () => onResultClick?.(result), children: jsxs("div", { className: "glass-flex glass-items-start glass-gap-4", children: [jsx("div", { className: "glass-text-2xl", children: getResultIcon(result.category) }), jsxs("div", { className: 'glass-flex-1 min-glass-w-0', children: [jsxs("div", { className: 'glass-flex glass-items-center glass-gap-3 mb-2', children: [jsx("h3", { className: 'glass-text-lg font-semibold glass-text-secondary', children: highlightText(result.title, result.highlights?.title) }), jsx("span", { className: "glass-px-2 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary glass-radius", children: result.category }), result.metadata?.rating && jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx("span", { className: 'text-primary', children: "\u2B50" }), jsx("span", { className: "glass-text-sm glass-text-secondary", children: result.metadata.rating })] })] }), jsx("p", { className: 'glass-text-secondary mb-3', children: highlightText(result.description, result.highlights?.description) }), result.tags.length > 0 && jsxs("div", { className: "glass-flex glass-flex-wrap glass-gap-1", children: [result.tags.slice(0, 5).map(tag => jsx("span", { className: 'glass-px-2 glass-py-1 glass-text-xs glass-surface-subtle text-primary glass-radius', children: tag }, tag)), result.tags.length > 5 && jsxs("span", { className: "glass-px-2 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary glass-radius", children: ["+", result.tags.length - 5, " more"] })] })] }), jsxs("div", { className: "glass-text-sm glass-text-secondary", children: ["Score: ", result.score.toFixed(1)] })] }) }, result.id)), results.length === 0 && (query.trim() || Object.keys(filters).length > 0) && !isSearching && jsxs("div", { className: 'text-center glass-py-12 glass-text-secondary', children: [jsx("div", { className: 'text-6xl mb-4', children: "\uD83D\uDD0D" }), jsx("h3", { className: 'glass-text-lg font-medium mb-2', children: "No results found" }), jsx("p", { className: "glass-text-sm", children: "Try adjusting your search terms or filters, or try using more general keywords." })] })] })] })] }); }; export { GlassIntelligentSearch }; //# sourceMappingURL=GlassIntelligentSearch.js.map