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 */}
نص مبسط
setUseUthmani(!useUthmani)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-300 focus:outline-none ${useUthmani ? 'bg-emerald-600' : 'bg-slate-200 dark:bg-slate-700'}`}
>
رسم عثماني
{view === 'search' ? (
{/* Search Tip Banner */}
لتحقيق أفضل تطابق، أدخل النص المراد البحث عنه بدقة .
{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( );