aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
631 lines (628 loc) • 26.2 kB
JavaScript
'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