const { useState, useEffect, useMemo, useRef } = React; // UI Components - Robust Icon with cleanup const Icon = ({ name, size = 20, className = "" }) => { const parentRef = useRef(null); useEffect(() => { let isMounted = true; if (window.lucide && parentRef.current) { try { parentRef.current.innerHTML = ``; window.lucide.createIcons({ root: parentRef.current }); } catch (e) { console.warn("Lucide error:", e); } } return () => { isMounted = false; }; }, [name, size, className]); return ; }; // Match Type Badge Component const MatchTypeBadge = ({ type }) => { const configs = { exact: { label: 'آية مشابهة', bg: 'bg-emerald-100', text: 'text-emerald-700', icon: 'check-circle' }, lemma: { label: 'اشتقاق لغوي', bg: 'bg-blue-100', text: 'text-blue-700', icon: 'git-branch' }, root: { label: 'جذر مشترك', bg: 'bg-amber-100', text: 'text-amber-700', icon: 'hash' }, similarity: { label: 'متشابهات', bg: 'bg-purple-100', text: 'text-purple-700', icon: 'copy' }, search_match: { label: 'نتيجة بحث', bg: 'bg-slate-100', text: 'text-slate-600', icon: 'search' } }; const config = configs[type] || configs.exact; return ( {config.label} ); }; // Unified Hint Component for subtle notifications const Hint = ({ type = 'info', title, children, icon, className = "" }) => { const themes = { info: { bg: 'bg-emerald-50/30 dark:bg-emerald-900/10', border: 'border-emerald-100/50 dark:border-emerald-800/20', text: 'text-emerald-900 dark:text-emerald-300', subText: 'text-emerald-700/70 dark:text-emerald-500', iconBg: 'bg-emerald-100 dark:bg-emerald-800', iconColor: 'text-emerald-600 dark:text-emerald-400', defaultIcon: 'lightbulb' }, warning: { bg: 'bg-amber-50/30 dark:bg-amber-900/10', border: 'border-amber-100/50 dark:border-amber-800/20', text: 'text-amber-900 dark:text-amber-300', subText: 'text-amber-700/70 dark:text-amber-600', iconBg: 'bg-amber-100 dark:bg-amber-800', iconColor: 'text-amber-600 dark:text-amber-400', defaultIcon: 'alert-circle' }, slate: { bg: 'bg-slate-50/50 dark:bg-slate-800/20', border: 'border-slate-200/50 dark:border-slate-700/30', text: 'text-slate-800 dark:text-slate-200', subText: 'text-slate-500 dark:text-slate-400', iconBg: 'bg-slate-100 dark:bg-slate-700', iconColor: 'text-slate-500 dark:text-slate-400', defaultIcon: 'info' } }; const theme = themes[type] || themes.info; return (
{title &&
{title}
}
{children}
); }; // Advanced Word-Level Differencing using LCS const VerseDiff = ({ text, comparisonText, highlightedTerms = [], large = false }) => { const words = useMemo(() => (text || "").split(/\s+/).filter(Boolean), [text]); const compWords = useMemo(() => (comparisonText || "").split(/\s+/).filter(Boolean), [comparisonText]); // Simple normalization for highlighting matches in Uthmanic text const normalize = (val) => { if (!val) return ""; return val .replace(/[\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E8\u06EA-\u06FC]/g, "") // Remove Tashkeel .replace(/[إأآٱ]/g, "ا") .replace(/[ؤئء]/g, "ء") .replace(/ة/g, "ه") .replace(/ى/g, "ي"); }; const getCommonWords = (w1, w2) => { if (!w1.length || !w2.length) return new Set(); const dp = Array(w1.length + 1).fill(0).map(() => Array(w2.length + 1).fill(0)); for (let i = 1; i <= w1.length; i++) { for (let j = 1; j <= w2.length; j++) { if (w1[i - 1] === w2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } const common = new Set(); let i = w1.length, j = w2.length; while (i > 0 && j > 0) { if (w1[i - 1] === w2[j - 1]) { common.add(`${i - 1}-${w1[i - 1]}`); i--; j--; } else if (dp[i - 1][j] > dp[i][j - 1]) i--; else j--; } return common; }; const commonSet = useMemo(() => comparisonText ? getCommonWords(words, compWords) : null, [words, compWords, comparisonText]); return (
{words.map((word, idx) => { const normalizedWord = normalize(word); const isHighlighted = highlightedTerms.some(term => { if (!term) return false; const normalizedTerm = normalize(term); return normalizedWord.includes(normalizedTerm); }); const isCommon = commonSet ? commonSet.has(`${idx}-${word}`) : true; // Styling logic based on comparison state let colorClass = 'text-slate-800 dark:text-slate-200'; if (comparisonText) { // When comparing, highlight commonalities in green colorClass = isCommon ? 'text-emerald-600 dark:text-emerald-400 font-black' : 'text-slate-800 dark:text-slate-200'; // Differences are now neutral } else if (isHighlighted) { // When not comparing, highlight search terms colorClass = 'text-emerald-600 dark:text-emerald-400 font-black'; } return ( {word}{' '} ); })}
); }; // Helper to calculate similarity score on the fly const SimilarityScore = ({ v, refText }) => { const score = useMemo(() => { if (v.score) return v.score; if (v.overlap) return v.overlap; if (!refText) return 0; // Fallback calculation similar to Engine's overlap ratio const normalize = (val) => (val || "").replace(/[\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E8\u06EA-\u06FC]/g, "").replace(/[إأآٱ]/g, "ا").replace(/[ؤئء]/g, "ء").replace(/ة/g, "ه").replace(/ى/g, "ي"); const words = normalize(v.text || v.standard).split(/\s+/).filter(Boolean); const refWords = normalize(refText).split(/\s+/).filter(Boolean); const refSet = new Set(refWords); const matches = words.filter(w => refSet.has(w)).length; return matches / Math.max(words.length, refWords.length); }, [v, refText]); if (!score) return null; return (
{Math.round(score * 100)}%
); }; // Sub-component for Similarity Card const SimilarityCard = ({ v, refVerseText, isPotential = false, useUthmani = false }) => (
{v.gid}

{v.sura_name || v.suraName} | {v.aya_id || v.verseId}

{isPotential ? ( تطابق بحث (محتمل) ) : ( )}
{!isPotential && }
); // Component for detail view const ComparisonDetail = ({ gid, onBack, searchResults = [], query = "", useUthmani = false }) => { const [mainVerse, setMainVerse] = useState(null); const [similarGroups, setSimilarGroups] = React.useState([]); const [loading, setLoading] = React.useState(true); const [intensity, setIntensity] = React.useState(5); const refreshSimilarities = React.useCallback(async (val) => { try { const groups = Engine.getSimilarGroup(gid, val) || []; setSimilarGroups(groups); } catch (err) { console.error(err); } }, [gid]); React.useEffect(() => { const loadVerse = async () => { setLoading(true); try { const verse = Engine.getVerse(gid); if (verse) { setMainVerse(verse); await refreshSimilarities(intensity); window.scrollTo({ top: 0, behavior: 'smooth' }); } } catch (err) { console.error("Detail load error:", err); } finally { setLoading(false); } }; loadVerse(); }, [gid, refreshSimilarities]); const handleIntensityChange = (e) => { const val = parseInt(e.target.value); setIntensity(val); // Invert logic: 1 (Right) -> 10 (Strict), 10 (Left) -> 1 (Wide) refreshSimilarities(11 - val); }; const refVerseText = useMemo(() => { if (!mainVerse) return ""; if (useUthmani && mainVerse.uthmani) return mainVerse.uthmani; return mainVerse.standard || mainVerse.text; }, [mainVerse, useUthmani]); const extraMatches = useMemo(() => { if (!searchResults.length || !mainVerse) return []; const existingGids = new Set(similarGroups.flatMap(g => g.verses).map(v => v.gid)); existingGids.add(mainVerse.gid); return searchResults .filter(r => !existingGids.has(r.gid) && r.matchType === 'exact') .map(r => ({ ...r, matchType: 'search_match' })); }, [searchResults, similarGroups, mainVerse]); const verifiedSimilars = useMemo(() => { return similarGroups.flatMap(g => g.verses).filter(v => v.gid !== gid); }, [similarGroups, gid]); if (loading) return (

تحميل بيانات الآية...

); return (
{mainVerse.verseId || mainVerse.aya_id}

سورة {mainVerse.suraName || mainVerse.sura_name}

الموضع المختار للمقارنة

المواضع المشابهة

درجة المطابقة {intensity <= 3 ? 'مطابقة دقيقة' : intensity >= 8 ? 'تشابه عريض' : 'مطابقة متوازنة'}
{intensity}
دقيق (نتائج أقل) واسع (نتائج أكثر)
{/* Verified Similarities Section */} {verifiedSimilars.length > 0 && (
تم حصر هذه المواضع بدقة كمتشابهات لهذه الآية بشكل شبه مؤكد.
{verifiedSimilars.map(v => ( ))}
)} {/* Empty State for Verified Similarities */} {verifiedSimilars.length === 0 && ( لم يتم العثور على روابط متشابهات محققة لهذه الآية في قاعدة البيانات. المواضع المعروضة أدناه (إن وجدت) هي نتائج من بحثك الحالي عن: "{query}" )} {/* Potential Search Matches Section */} {extraMatches.length > 0 && (
هذه المواضع ظهرت في بحثك عن "{query}" وقد تكون مشابهة، لكن لم يتم تصنيفها كمتشابهات مؤكدة.
{extraMatches.map(v => ( ))}
)}
); }; const App = () => { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); const [view, setView] = useState('search'); const [selectedGid, setSelectedGid] = useState(null); const [error, setError] = useState(null); const [useUthmani, setUseUthmani] = useState(false); const [deferredPrompt, setDeferredPrompt] = useState(null); useEffect(() => { const handleBeforeInstallPrompt = (e) => { e.preventDefault(); setDeferredPrompt(e); }; window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); return () => window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); }, []); const handleInstallClick = async () => { if (!deferredPrompt) return; deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { setDeferredPrompt(null); } }; useEffect(() => { const init = async () => { try { if (typeof Engine === 'undefined') throw new Error('Engine loading failed'); await Engine.init(); setLoading(false); } catch (err) { setError(err.message); setLoading(false); } }; init(); }, []); const handleSearch = (val) => { setQuery(val); }; useEffect(() => { if (!query.trim()) { setResults([]); setSearching(false); return; } setSearching(true); const timer = setTimeout(() => { try { const res = Engine.search(query); setResults(res); } catch (e) { console.error("Search error:", e); } finally { setSearching(false); } }, 400); // 400ms debounce delay return () => clearTimeout(timer); }, [query]); if (loading) return (

جاري التحميل...

); return (

فُروق

البحث البصري في المتشابهات

{deferredPrompt && ( )} {/* Uthmani Toggle Switch */}
نص مبسط رسم عثماني
{view === 'search' ? (
{/* Search Tip Banner */} لتحقيق أفضل تطابق، أدخل النص المراد البحث عنه بدقة.
{searching ? : }
handleSearch(e.target.value)} className="w-full h-16 pr-14 pl-6 bg-white dark:bg-slate-800 rounded-3xl shadow-xl border-2 border-transparent focus:border-emerald-500 outline-none text-xl font-cairo" />
{results.map((verse) => (
{ setSelectedGid(verse.gid); setView('detail'); }} >
{verse.gid}

سورة {verse.suraName || verse.sura_name} | {verse.verseId || verse.aya_id}

{verse.matchScore?.toFixed(1)} نقطة
مواضع التشابه
))}
) : ( setView('search')} searchResults={results} query={query} useUthmani={useUthmani} /> )}
); }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render();