khaterekhan2 / script.js
Jahadona's picture
Update script.js
8a4e963 verified
// این فایل script.js نسخه نهایی شده بر اساس کد اصلی شماست.
// شامل تمام اصلاحات لازم برای کار در Hugging Face Space جدید با مدل heydariAI/persian-embeddings.
// تمام قابلیت های اضافه شده و رفع اشکالات شناسایی شده در فرآیند عیب یابی در آن لحاظ شده است.
// این نسخه شامل مدیریت بصری وضعیت (لودینگ، آماده) با رنگ و اسپینر است.
// ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ******
// این متغیرها در بلوک DOMContentLoaded پس از بارگذاری صفحه مقداردهی اولیه می شوند.
let searchButton;
let userQuestionInput; // <--- مطابق با ID در index.html شما
let searchResultsContainer; // <--- مطابق با ID در index.html شما
let loadingStatusElement; // <--- مطابق با ID در index.html شما
let selectionErrorElement; // <--- مطابق با ID در index.html شما
let selectAllCheckbox; // <--- مطابق با ID در index.html شما
let bookCheckboxes; // <--- مطابق با class در index.html شما (HTMLCollection از المان ها)
let resultsPerPageSelect; // <--- مطابق با ID در index.html شما
let similarityThresholdInput; // <--- مطابق با ID در index.html شما
let loadingSpinnerElement; // <--- متغیر برای المان لودینگ اسپینر
// *************************************************
// ****** تعریف URL سرور پایتون برای دریافت Embedding سوال ******
// آدرس نسبی برای ارتباط با Backend در همان سرور Flask که Frontend را سرویس می دهد.
const EMBEDDING_SERVER_URL = '/get_embedding'; // <--- آدرس نسبی برای Space مشترک
// ****** نگاشت نام فایل JSON به نام کامل کتاب (برای نمایش در نتایج و فیلتر) ******
// این map اطلاعات نمایشی کتاب را بر اساس value چک باکس (نام فایل JSON) فراهم می کند.
// مطمئن شوید که value چک باکس ها در index.html شما با کلیدهای این map مطابقت دارد.
const bookInfo = {
// 'نام_فایل_json_در_value_چک_باکس': 'نام کامل نمایشی کتاب',
'jabe_siah.json': 'جعبه سیاه (منتخب خاطرات اسدالله علم)', // مثال: value چک باکس : نام نمایشی کتاب
// اگر چک باکس های بیشتری در HTML دارید و فایل های JSON متناظر دارید، ورودی های متناظر را اینجا اضافه کنید:
// 'ketab_dovom.json': 'نام کتاب دوم',
// 'ketab_sevom.json': 'نام کتاب سوم',
// مطمئن شوید که فایل های JSON متناظر (مثلا ketab_dovom.json) در Space آپلود شده اند.
};
// ****** آستانه شباهت پیش فرض ******
// اگر عنصر ورودی آستانه پیدا نشد یا مقدار آن نامعتبر بود، از این مقدار استفاده می شود.
const DEFAULT_SIMILARITY_THRESHOLD = 0.15;
// ****** متغیر سراسری برای نگهداری داده های بارگذاری شده از کتاب های انتخاب شده ******
let memoirsWithEmbeddings = []; // این آرایه حاوی خاطرات با بردارهای embedding از کتاب های انتخاب شده است.
// ****** توابع کمکی برای مدیریت پیام ها و وضعیت UI ******
// تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش در المان loadingStatusElement
// Optional state parameter: 'loading', 'ready', 'error', '' (یا هر پیام دیگر که وضعیت خاصی را نشان ندهد)
function updateStatus(message, isError = false, state = '') {
if (loadingStatusElement) {
loadingStatusElement.textContent = message; // Set text first
// پاک کردن کلاس های وضعیت قبلی
loadingStatusElement.classList.remove('loading', 'ready', 'error');
// اضافه کردن کلاس وضعیت مناسب بر اساس isError یا پارامتر state
if (isError) {
loadingStatusElement.classList.add('error');
// رنگ و فونت ضخیم توسط کلاس .error-message یا .status-message.error در CSS مدیریت می شود
loadingStatusElement.style.color = ''; // ریست کردن استایل های اینلاین احتمالی
loadingStatusElement.style.fontWeight = ''; // ریست کردن استایل های اینلاین احتمالی
} else if (state === 'loading') {
loadingStatusElement.classList.add('loading');
loadingStatusElement.style.color = '';
loadingStatusElement.style.fontWeight = '';
} else if (state === 'ready') {
loadingStatusElement.classList.add('ready');
loadingStatusElement.style.color = '';
loadingStatusElement.style.fontWeight = '';
} else {
// وضعیت پیش فرض یا پیام های عادی بدون وضعیت خاص
loadingStatusElement.style.color = '#666'; // رنگ پیش فرض
loadingStatusElement.style.fontWeight = 'normal'; // فونت نرمال پیش فرض
}
// مدیریت نمایش اسپینر (اسپینر با CSS و کلاس .status-message.loading کنترل می شود)
// نیازی به نمایش/پنهان کردن مستقیم اسپینر اینجا نیست اگر CSS به درستی تنظیم شده باشد.
loadingStatusElement.style.display = message ? 'flex' : 'none'; // استفاده از flex برای نمایش و مرکز کردن محتوا (متن و اسپینر)
// اگر پیام خالی باشد، المان مخفی می شود.
} else {
console.log("Status:", message, "isError:", isError, "state:", state); // لاگ برای توسعه
}
// پاک کردن پیام خطای انتخاب کتاب اگر یک پیام وضعیت نمایش داده می شود
if (message && selectionErrorElement) {
selectionErrorElement.textContent = '';
selectionErrorElement.style.display = 'none';
selectionErrorElement.classList.remove('error'); // پاک کردن کلاس خطا
}
}
// تابع کمکی برای نمایش خطای انتخاب کتاب یا بارگذاری داده در المان selectionErrorElement
function updateSelectionError(message) {
if (selectionErrorElement) {
selectionErrorElement.textContent = message;
selectionErrorElement.style.color = 'red'; // رنگ قرمز
selectionErrorElement.style.fontWeight = 'bold'; // فونت ضخیم
selectionErrorElement.style.display = message ? 'flex' : 'none'; // نمایش با فلکس
selectionErrorElement.classList.add('error'); // اضافه کردن کلاس خطا
} else {
console.error("Selection Error Element not found."); // لاگ برای توسعه
}
// پاک کردن پیام وضعیت اگر یک پیام خطای انتخاب نمایش داده می شود
if (message && loadingStatusElement) {
loadingStatusElement.textContent = '';
loadingStatusElement.style.display = 'none';
loadingStatusElement.classList.remove('loading', 'ready'); // پاک کردن کلاس های وضعیت
// اسپینر نیز توسط CSS و کلاس مدیریت می شود.
}
}
// تابع کمکی برای فعال/غیرفعال کردن دکمه جستجو
function setButtonEnabled(enabled) {
if (searchButton) {
searchButton.disabled = !enabled;
// console.log("Search button enabled state:", enabled); // لاگ برای توسعه
} else {
console.error("Search button element not found.");
}
}
// ****** تابع برای بررسی وضعیت و فعال/غیرفعال کردن دکمه جستجو ******
// دکمه فقط زمانی فعال می شود که داده بارگذاری شده، کادر سوال خالی نیست و آستانه معتبر است.
function checkAndEnableSearchButton() {
const isDataLoaded = memoirsWithEmbeddings.length > 0;
// اطمینان از وجود userQuestionInput قبل از دسترسی به value
const isQueryNotEmpty = userQuestionInput && userQuestionInput.value.trim() !== '';
// همچنین بررسی می کنیم که مقدار آستانه شباهت معتبر باشد اگر عنصر ورودی وجود دارد
let isThresholdInputValid = true;
if (similarityThresholdInput) {
const inputValue = parseFloat(similarityThresholdInput.value);
// چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد
if (isNaN(inputValue) || inputValue < 0.0 || inputValue > 1.0) {
isThresholdInputValid = false; // مقدار نامعتبر است
}
} else {
// اگر عنصر ورودی پیدا نشد، از آستانه پیش فرض استفاده می کنیم و فرض می کنیم معتبر است
console.warn("Similarity threshold input element not found. Assuming default threshold is valid.");
}
// دکمه فقط زمانی فعال می شود که تمام شرایط لازم برقرار باشد
setButtonEnabled(isDataLoaded && isQueryNotEmpty && isThresholdInputValid);
}
// ****** تابع اصلی برای بارگذاری داده‌ها از فایل‌های JSON کتاب‌های انتخاب شده ******
// این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند.
async function updateSelectedBooksData() {
console.log("Updating selected books data..."); // لاگ برای توسعه
// تنظیم وضعیت به "در حال بارگذاری" با رنگ و اسپینر
updateStatus("در حال بارگذاری داده‌ها...", false, 'loading');
updateSelectionError(""); // پاک کردن پیام خطاهای قبلی
setButtonEnabled(false); // غیرفعال کردن دکمه جستجو
if (searchResultsContainer) { // پاک کردن نتایج قبلی
// نمایش پیام اولیه واضح در بخش نتایج
searchResultsContainer.innerHTML = '<p>در حال بارگذاری اطلاعات کتاب‌ها. لطفاً صبر کنید تا دکمه جستجو فعال شود...</p>';
}
// پیدا کردن چک باکس های کتاب ها که انتخاب شده اند
// از getElementsByClassName استفاده می کنیم و آن را به آرایه تبدیل می کنیم
if (!bookCheckboxes || bookCheckboxes.length === 0) {
console.error("No book checkboxes found with class 'book-checkbox'. Cannot load data.");
updateStatus(""); // پاک کردن پیام وضعیت
updateSelectionError("المان‌های انتخاب کتاب یافت نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا
memoirsWithEmbeddings = [];
checkAndEnableSearchButton();
return;
}
// value چک باکس ها همان نام فایل JSON است که باید بارگذاری شود.
const selectedBookFiles = Array.from(bookCheckboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
console.log("Selected book files (from checkbox values):", selectedBookFiles); // لاگ برای توسعه
// ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ******
if (selectedBookFiles.length === 0) {
updateStatus(""); // پاک کردن پیام وضعیت
updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا
console.warn("No books selected. Cannot load data."); // لاگ
memoirsWithEmbeddings = []; // پاک کردن داده های قبلی
checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
return; // توقف فرآیند
}
// ********************************************************
memoirsWithEmbeddings = []; // پاک کردن داده‌های قبلی قبل از بارگذاری جدید
try {
// بارگذاری همزمان تمام فایل‌های JSON انتخاب شده
const fetchPromises = selectedBookFiles.map(filename => {
// آدرس فایل JSON نسبت به محل قرارگیری index.html
// فرض بر این است که فایل های JSON در کنار index.html در همان پوشه قرار دارند
const filePath = `./${filename}`;
console.log(`Attempting to fetch: ${filePath}`); // لاگ برای توسعه
return fetch(filePath).then(response => {
if (!response.ok) {
// اگر فایل پیدا نشد یا خطای دیگری رخ داد (مثلاً 404)
console.error(`Workspace error for ${filePath}: Status ${response.status}`); // لاگ خطا
// تلاش برای خواندن متن خطا از پاسخ
return response.text().then(text => {
console.error(`Response body for ${filePath}: ${text}`);
throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
}).catch(() => {
throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
});
}
// بررسی Content-Type پاسخ
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
console.error(`Workspace error for ${filePath}: Expected application/json, but received ${contentType}`); // لاگ خطا
throw new Error(`Error fetching file: "${filename}". Expected JSON, but received unexpected content type.`);
}
return response.json();
})
.catch(error => {
// گرفتن خطاهای شبکه یا تجزیه JSON
console.error(`Failed to fetch or parse file "${filename}":`, error); // لاگ خطا
throw new Error(`Failed to load data for book file "${filename}". ${error.message || 'Unknown error'}. Check file name and location.`);
});
});
const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها
// ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده و افزودن اطلاعات کتاب
booksData.forEach((data, index) => { // اضافه کردن index
const filename = selectedBookFiles[index]; // نام فایل JSON فعلی
// پیدا کردن نام نمایشی کتاب از map bookInfo
const bookDisplayName = bookInfo[filename] || filename;
if (Array.isArray(data)) {
// فیلتر کردن آیتم هایی که بردار embedding معتبر دارند و افزودن اطلاعات کتاب به آن ها
const memoirsWithBookInfo = data.filter(item => item && typeof item === 'object' && item.embedding && Array.isArray(item.embedding) && item.embedding.length > 0)
.map(item => ({
...item, // کپی کردن تمام پراپرتی های موجود (passage_original, passage_combined, reference, embedding, etc.)
book_title: bookDisplayName, // افزودن نام نمایشی
book_file: filename // افزودن نام فایل
}));
memoirsWithEmbeddings = memoirsWithEmbeddings.concat(memoirsWithBookInfo);
// هشدار برای آیتم های نامعتبر
const invalidMemoirsCount = data.length - memoirsWithBookInfo.length;
if(invalidMemoirsCount > 0){
console.warn(`Skipped ${invalidMemoirsCount} items from file "${filename}" due to missing or invalid embedding or format.`); // لاگ هشدار
}
} else {
console.error(`Loaded data from "${filename}" is not an array:`, data); // لاگ خطا
updateSelectionError(`خطا در فرمت داده از فایل "${filename}". انتظار آرایه داشتیم.`); // پیام خطا
}
});
const loadedBooksCount = selectedBookFiles.length;
const totalPassagesLoaded = memoirsWithEmbeddings.length;
if (totalPassagesLoaded === 0) {
console.warn("No valid passages with embeddings were loaded..."); // لاگ هشدار
// تنظیم وضعیت به "خطا در بارگذاری"
updateStatus(`داده‌ها بارگذاری شد، اما هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده (${loadedBooksCount} کتاب) یافت نشد.`, true, 'error');
updateSelectionError("هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده یافت نشد. فرمت فایل‌های JSON و وجود فیلد embedding را بررسی کنید."); // پیام خطا
} else {
console.log(`Successfully loaded data from ${loadedBooksCount} book(s)...`); // لاگ
// تنظیم وضعیت به "آماده"
updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات قابل جستجو: ${totalPassagesLoaded}. آماده جستجو هستید.`, false, 'ready');
// ****** پاک کردن پیام اولیه در بخش نتایج پس از بارگذاری موفق داده ******
// این خط باعث می شود که پس از بارگذاری موفقیت آمیز داده ها، پیام "در حال بارگذاری..." از بخش نتایج پاک شود.
if (searchResultsContainer) { // چک کردن وجود المان نتایج
// بررسی اینکه آیا پیام فعلی در بخش نتایج همان پیام لودینگ اولیه است یا خیر
// این کار از پاک شدن نتایج جستجو در صورت بارگذاری مجدد پس از جستجو جلوگیری می کند
if (searchResultsContainer.innerHTML.includes('در حال بارگذاری اطلاعات کتاب‌ها. لطفاً صبر کنید')) {
searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // بازگرداندن به پیام پیش فرض بخش نتایج
}
}
// ************************************************************************
}
checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه
} catch (error) {
console.error("Error loading selected books data:", error); // لاگ خطا
// تنظیم وضعیت به "خطا در بارگذاری"
updateStatus("خطا در بارگذاری داده‌ها.", true, 'error');
// پیام خطای برای کاربر شامل جزئیات بیشتر
updateSelectionError(`خطا در بارگذاری داده‌ها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`);
memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
if (searchResultsContainer) { // بازگرداندن پیام اولیه
searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>';
}
} finally {
// checkAndEnableSearchButton در هر دو شاخه try/catch صدا زده می شود.
}
}
// ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ******
function cosineSimilarity(vecA, vecB) {
// بررسی وجود و صحت بردارها
if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
console.error("Cosine Similarity Error: Invalid or empty vectors provided.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'}); // لاگ خطا
return 0; // برگرداندن 0
}
let dotProduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
// محاسبه نقطه ضرب و مربع اندازه
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
magnitudeA += vecA[i] * vecA[i];
magnitudeB += vecB[i] * vecB[i];
}
// گرفتن ریشه دوم
magnitudeA = Math.sqrt(magnitudeA);
magnitudeB = Math.sqrt(magnitudeB);
// جلوگیری از تقسیم بر صفر
if (magnitudeA === 0 || magnitudeB === 0) {
return 0;
}
// محاسبه شباهت
const similarity = dotProduct / (magnitudeA * magnitudeB);
return similarity; // برگرداندن امتیاز
}
// تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage_combined
// این تابع فعلاً برای نمایش passage_original مستقیماً استفاده نمی شود، اما نگه داشته شده است.
function cleanPassageTextForDisplay(passage) {
if (!passage || typeof passage !== 'string') {
console.warn("cleanPassageTextForDisplay received invalid input:", passage); // لاگ هشدار
return ''; // برگرداندن رشته خالی
}
const startDelimiter = ' <کلیدواژه ها: ';
const startIndex = passage.indexOf(startDelimiter);
if (startIndex === -1) {
return passage.trim(); // اگر جداکننده پیدا نشد
}
let cleanText = passage.substring(0, startIndex);
return cleanText.trim(); // حذف فاصله های اضافی
}
// ****** تعریف کامل تابع async function searchMemoirs() { ... } ******
async function searchMemoirs() {
console.log("Search triggered."); // لاگ
console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ
// ****** چک کردن اینکه داده ها بارگذاری شده باشند ******
if (memoirsWithEmbeddings.length === 0) {
console.warn("No memoir data loaded. Cannot search."); // لاگ هشدار
updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا
return;
}
// **************************************************************************
// اطمینان از وجود userQuestionInput
if (!userQuestionInput) {
console.error("Search input element not found."); // لاگ خطا
updateSelectionError("المان ورودی جستجو پیدا نشد. امکان جستجو وجود ندارد."); // پیام خطا
return;
}
const query = userQuestionInput.value.trim();
console.log(`Query text is: "${query}"`); // لاگ
if (!query) {
if (searchResultsContainer) {
searchResultsContainer.innerHTML = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام
}
console.warn("Search query is empty."); // لاگ هشدار
updateSelectionError("لطفاً عبارت مورد نظر برای جستجو را وارد کنید."); // پیام خطا
return;
}
// نمایش پیام "در حال جستجو" با وضعیت لودینگ
updateStatus("در حال جستجو...", false, 'loading');
updateSelectionError(""); // پاک کردن خطاهای قبلی
if (searchResultsContainer) { // پاک کردن نتایج قبلی
searchResultsContainer.innerHTML = '';
}
setButtonEnabled(false); // غیرفعال کردن دکمه
try {
console.log("Requesting query embedding from Backend..."); // لاگ
// ارسال درخواست به Backend
const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: query })
});
// بررسی موفقیت آمیز بودن پاسخ
if (!serverResponse.ok) {
const errorBody = await serverResponse.text(); // خواندن متن خطا
console.error(`Backend responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا
let serverErrorMessage = `خطا از Backend (${serverResponse.status}).`;
try { // تلاش برای خواندن JSON خطا
const errorJson = JSON.parse(errorBody);
if (errorJson.error) {
serverErrorMessage += ` پیام: ${errorJson.error}`;
}
} catch (e) { // اگر JSON نبود
serverErrorMessage += ` پاسخ خام: ${errorBody.substring(0, Math.min(errorBody.length, 100))}...`;
}
// تنظیم وضعیت به "خطا"
updateStatus("جستجو با خطا مواجه شد.", true, 'error');
updateSelectionError(serverErrorMessage + " جزئیات بیشتر در کنسول مرورگر."); // نمایش خطا
return; // خروج
}
// دریافت و بررسی پاسخ JSON
const serverData = await serverResponse.json();
const queryEmbeddingArray = serverData.embedding;
if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
console.error("Backend returned an invalid or empty embedding:", serverData); // لاگ خطا
// تنظیم وضعیت به "خطا"
updateStatus("جستجو با خطا مواجه شد.", true, 'error');
updateSelectionError("Backend بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // نمایش خطا
return; // خروج
}
console.log("Query embedding received from Backend successfully."); // لاگ
console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ
console.log("Calculating similarities in browser..."); // لاگ
// محاسبه شباهت با تمام خاطرات
const searchResults = [];
for (const memoir of memoirsWithEmbeddings) {
// اطمینان از وجود و صحت بردار embedding و تطابق ابعاد
if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
searchResults.push({ ...memoir, similarity: similarity });
} else {
console.warn(`Skipping memoir due to missing or invalid embedding or dimension mismatch: ${memoir.book_title || memoir.book_file || 'Unknown Book'} - ${memoir.reference || 'Unknown Reference'}`); // لاگ هشدار
}
}
console.log(`Similarity calculation complete. Found ${searchResults.length} results...`); // لاگ
// ****** فیلتر کردن نتایج بر اساس آستانه شباهت ******
let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD;
if (similarityThresholdInput) {
const inputValue = parseFloat(similarityThresholdInput.value);
if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) {
currentSimilarityThreshold = inputValue;
console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ
} else {
console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار
updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا
currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD;
}
} else {
console.warn("Similarity threshold input element not found...", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار
}
const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold);
console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ
// *************************************************************
console.log("Sorting results by similarity..."); // لاگ
filteredResults.sort((a, b) => b.similarity - a.similarity);
console.log("Filtered results sorted."); // لاگ
// ****** انتخاب تعداد نتایج برتر ******
let finalResultsPerPage = 10;
if (resultsPerPageSelect) {
const selectedValue = parseInt(resultsPerPageSelect.value, 10);
finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10;
} else {
console.warn("Results per page select element not found...", finalResultsPerPage); // لاگ هشدار
}
const topResults = filteredResults.slice(0, finalResultsPerPage);
console.log(`Displaying top ${topResults.length} results...`); // لاگ
// لاگ کردن نتایج برتر برای عیب یابی (اختیاری)
// console.log("Top results data before display:", topResults);
// ****** منطق نمایش نتایج در HTML ******
if (searchResultsContainer) {
if (topResults.length === 0) {
searchResultsContainer.innerHTML = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`;
console.log("No relevant results found..."); // لاگ
} else {
console.log("Results found, updating DOM.");
const resultsList = document.createElement('div');
resultsList.classList.add('results-list');
topResults.forEach(result => {
const resultItem = document.createElement('div');
resultItem.classList.add('result-item');
// نمایش امتیاز شباهت (float right دارد، ترتیب آن در اینجا تأثیر کمتری بر ترتیب بلوک اصلی دارد)
const similarityElement = document.createElement('p');
similarityElement.classList.add('result-similarity');
similarityElement.textContent = `شباهت: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}`;
resultItem.appendChild(similarityElement); // اضافه کردن امتیاز شباهت (معمولاً اول یا زودتر)
// ****** نمایش المان‌ها به ترتیب مورد نظر: خاطره، مرجع، نام کتاب ******
// ۱. نمایش متن خاطره
const passageElement = document.createElement('p');
passageElement.classList.add('result-passage');
passageElement.textContent = result.passage_original || 'متن خاطره موجود نیست.'; // استفاده از passage_original
resultItem.appendChild(passageElement); // اضافه کردن خاطره (اولین)
// ۲. نمایش مرجع خاطره
const referenceElement = document.createElement('p');
referenceElement.classList.add('result-reference');
referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`;
resultItem.appendChild(referenceElement); // اضافه کردن مرجع (دومین)
// ۳. نمایش نام کتاب
const bookTitleElement = document.createElement('p');
bookTitleElement.classList.add('result-book-title');
bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
resultItem.appendChild(bookTitleElement); // اضافه کردن نام کتاب (سومین)
// ********************************************************************************
// ****** اضافه کردن دکمه کپی (بعد از محتوای اصلی) ******
const copyButton = document.createElement('button');
copyButton.classList.add('copy-button');
copyButton.textContent = 'کپی متن';
// استایل های موقت Inline برای تست - بهتر است در style.css تعریف شوند و کلاس copy-button به آنها لینک شود
// در style.css ارائه شده قبلی این استایل ها موجود هستند و نیازی به اینجا نیست
// copyButton.style.display = 'block';
// copyButton.style.marginTop = '15px';
// copyButton.style.marginLeft = 'auto'; // راست چین کردن در RTL
// copyButton.style.marginRight = '0';
// copyButton.style.backgroundColor = '#007bff';
// copyButton.style.color = 'white';
// copyButton.style.padding = '5px 10px';
// copyButton.style.border = 'none';
// copyButton.style.borderRadius = '4px';
// copyButton.style.cursor = 'pointer';
// copyButton.style.fontSize = '0.85em';
// copyButton.style.fontFamily = "'Vazirmatn', sans-serif";
// **********************************
// Listener برای دکمه کپی به صورت Delegation در DOMContentLoaded اضافه شده است و نیازی به تعریف مجدد در اینجا نیست.
resultItem.appendChild(copyButton); // اضافه کردن دکمه کپی (آخرین)
// **************************************************************************
resultsList.appendChild(resultItem); // اضافه کردن آیتم نتیجه کامل شده به لیست نتایج
}); // پایان حلقه forEach
searchResultsContainer.appendChild(resultsList);
console.log("DOM updated with results.");
// لاگ کردن نتایج برتر نمایش داده شده (اختیاری)
console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`);
topResults.forEach(result => {
console.log(` Book: ${result.book_title || 'Unknown'}, Ref: ${result.reference || 'N/A'}, Sim: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}, Passage: "${result.passage_original ? result.passage_original.substring(0, Math.min(result.passage_original.length, 50)).replace(/\n/g, ' ') + '...' : 'N/A'}"`);
});
}
// به‌روزرسانی پیام وضعیت پس از جستجو
if (topResults.length > 0) {
updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر (پس از فیلتر با آستانه ${currentSimilarityThreshold.toFixed(2)}) نمایش داده شد.`, false, 'ready'); // وضعیت "آماده"
} else {
updateStatus(`جستجو به پایان رسید. هیچ نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.`, false, 'ready'); // وضعیت "آماده"
}
} else {
console.error("Could not find searchResultsContainer to display results."); // لاگ خطا
updateStatus("جستجو با خطا مواجه شد.", true, 'error'); // وضعیت "خطا"
updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا
}
} catch (error) {
// مدیریت خطا هنگام درخواست به Backend یا پردازش پاسخ
console.error("Error during search:", error); // لاگ خطا
if (searchResultsContainer) {
searchResultsContainer.innerHTML = `<p style=\"color: red;\">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
}
// تنظیم وضعیت به "خطا"
updateStatus("جستجو با خطا مواجه شد.", true, 'error');
updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطا
} finally {
// در نهایت دکمه جستجو وضعیت خود را بررسی می کند.
checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو (اگر شرایط فراهم باشد)
}
}
// ****** بلوک DOMContentLoaded: این کد پس از بارگذاری کامل ساختار صفحه اجرا می شود ******
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM fully loaded and parsed. Initializing script."); // لاگ
// ****** دریافت رفرنس المان های HTML (مطابق با ID ها و کلاس ها در index.html شما) ******
// این رفرنس ها متغیرهای سراسری تعریف شده در بالای فایل هستند.
searchButton = document.getElementById('searchButton');
userQuestionInput = document.getElementById('userQuestion'); // <--- مطابق با ID
searchResultsContainer = document.getElementById('searchResults'); // <--- مطابق با ID
loadingStatusElement = document.getElementById('loadingStatus'); // <--- مطابق با ID
selectionErrorElement = document.getElementById('selectionError'); // <--- مطابق با ID
selectAllCheckbox = document.getElementById('select_all_books'); // <--- مطابق با ID
// getElementsByClassName برمی گرداند HTMLCollection زنده.
bookCheckboxes = document.getElementsByClassName('book-checkbox'); // <--- مطابق با class
resultsPerPageSelect = document.getElementById('resultsPerPage'); // <--- مطابق با ID
similarityThresholdInput = document.getElementById('similarityThresholdInput'); // <--- مطابق با ID
loadingSpinnerElement = document.querySelector('#loadingStatus .loading-spinner'); // <--- دریافت رفرنس اسپینر
// **********************************************************************************
// ****** چک کردن وجود المان های ضروری برای جلوگیری از خطا ******
// چک می کنیم که تمام المان های مورد نیاز برای اجرای اسکریپت پیدا شده باشند.
// bookCheckboxes باید وجود داشته باشد و حداقل یک المان (چک باکس) داشته باشد.
const requiredElementsFound = searchButton && userQuestionInput && searchResultsContainer && loadingStatusElement && selectionErrorElement && selectAllCheckbox && bookCheckboxes && bookCheckboxes.length > 0 && resultsPerPageSelect && similarityThresholdInput && loadingSpinnerElement;
if (requiredElementsFound) {
console.log("All critical DOM elements found. Proceeding with initialization."); // لاگ
// ****** تنظیم Event Listeners ******
// Listener برای دکمه جستجو: فراخوانی تابع searchMemoirs هنگام کلیک
searchButton.addEventListener('click', searchMemoirs);
console.log("Search button click listener added."); // لاگ
// Listener برای کلید Enter در کادر ورودی سوال: شبیه سازی کلیک روی دکمه جستجو
userQuestionInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
event.preventDefault(); // جلوگیری از ارسال فرم
if (!searchButton.disabled) { // فقط اگر دکمه فعال است
searchButton.click();
console.log("Enter key pressed in search input, simulating search button click."); // لاگ
}
}
});
console.log("Search input keypress listener added."); // لاگ
// ****** Listener برای تغییر محتوای کادر سوال: بررسی وضعیت دکمه جستجو ******
// این Listener باعث می شود دکمه جستجو زمانی که متن وارد می شود فعال شود.
userQuestionInput.addEventListener('input', () => {
console.log("Search input value changed, checking button state."); // لاگ
checkAndEnableSearchButton(); // <--- فراخوانی تابع بررسی وضعیت دکمه
});
console.log("Search input 'input' listener added.");
// *********************************************************************
// Listener برای تغییر مقدار ورودی آستانه شباهت: به‌روزرسانی وضعیت دکمه
similarityThresholdInput.addEventListener('input', () => {
console.log("Similarity threshold input value changed."); // لاگ
checkAndEnableSearchButton(); // بررسی وضعیت دکمه (اگر مقدار نامعتبر شود غیرفعال می شود)
});
console.log("Similarity threshold input listener added."); // لاگ
// Listener برای تغییر انتخاب تعداد نتایج در صفحه
resultsPerPageSelect.addEventListener('change', () => {
console.log("Results per page setting changed."); // لاگ
// نیازی به checkAndEnableSearchButton نیست.
});
console.log("Results per page select listener added."); // لاگ
// Listener ها برای چک باکس 'انتخاب همه' و چک باکس های تکی کتاب ها
// تغییر در این چک باکس ها باید منجر به بارگذاری مجدد داده ها شود.
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', () => {
const isChecked = selectAllCheckbox.checked;
Array.from(bookCheckboxes).forEach(cb => {
cb.checked = isChecked;
});
console.log(`'Select All' checkbox changed to ${isChecked}. All book checkboxes updated.`); // لاگ
updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
});
console.log("'Select All' checkbox change listener added."); // لاگ
} else {
console.error("'Select All' checkbox element with ID 'select_all_books' not found."); // لاگ خطا
}
if (bookCheckboxes && bookCheckboxes.length > 0) {
Array.from(bookCheckboxes).forEach(cb => {
cb.addEventListener('change', () => {
const allOthersChecked = Array.from(bookCheckboxes).every(cb => cb.checked);
if (selectAllCheckbox) {
selectAllCheckbox.checked = allOthersChecked;
}
console.log("Individual book checkbox changed. Checking 'Select All' status."); // لاگ
updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
});
});
console.log("Individual book checkboxes change listeners added."); // لاگ
} else {
console.warn("No book checkboxes found with class 'book-checkbox'..."); // لاگ هشدار
}
// Listener برای دکمه های کپی (به صورت Delegation)
searchResultsContainer.addEventListener('click', (event) => {
if (event.target && event.target.classList && event.target.classList.contains('copy-button')) {
event.preventDefault(); // جلوگیری از رفتار پیش فرض
const resultItemElement = event.target.closest('.result-item');
if (resultItemElement) {
// استخراج متن از المان های نمایش داده شده
const passageText = resultItemElement.querySelector('.result-passage')?.textContent || '';
const referenceText = resultItemElement.querySelector('.result-reference')?.textContent.replace('مرجع:', '').trim() || 'نامشخص';
const bookTitleText = resultItemElement.querySelector('.result-book-title')?.textContent.replace('از کتاب:', '').trim() || 'نامشخص';
// گرفتن متن کامل عنصر شباهت (مثلا "شباهت: 0.7500")
const similarityElementText = resultItemElement.querySelector('.result-similarity')?.textContent || '';
// ساخت متنی که باید کپی شود
const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\n${similarityElementText}`;
// کپی کردن متن به کلیپ بورد
navigator.clipboard.writeText(textToCopy)
.then(() => {
event.target.textContent = 'کپی شد!';
setTimeout(() => {
event.target.textContent = 'کپی متن';
}, 2000);
console.log("Passage, reference, book title, and similarity copied."); // لاگ
})
.catch(err => {
console.error('Failed to copy text: ', err); // لاگ خطا
event.target.textContent = 'خطا در کپی';
setTimeout(() => {
event.target.textContent = 'کپی متن';
}, 2000);
});
} else {
console.warn("Result item parent not found for copy button click."); // لاگ هشدار
}
}
});
console.log("Copy button delegation click listener added."); // لاگ
// ****** راه اندازی اولیه: بارگذاری داده ها هنگام بارگذاری صفحه ******
// این تابع فرآیند بارگذاری داده از فایل های JSON بر اساس چک باکس های پیش فرض انتخاب شده در HTML را شروع می کند.
updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند.
console.log("Initial data loading process started."); // لاگ
} else {
// اگر تمام عناصر مورد نیاز پیدا نشدند
const errorMessage = "خطا در بارگذاری صفحه: برخی یا تمام عناصر لازم (HTML) پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید و مطمئن شوید همه عناصر ضروری وجود دارند."; // پیام خطا برای کاربر
console.error(errorMessage, { // لاگ جزئیات خطا
searchButton: !!searchButton,
userQuestionInput: !!userQuestionInput,
searchResultsContainer: !!searchResultsContainer,
loadingStatusElement: !!loadingStatusElement,
selectionErrorElement: !!selectionErrorElement,
selectAllCheckbox: !!selectAllCheckbox,
bookCheckboxesCount: bookCheckboxes ? bookCheckboxes.length : 0,
resultsPerPageSelect: !!resultsPerPageSelect,
similarityThresholdInput: !!similarityThresholdInput,
loadingSpinnerElement: !!loadingSpinnerElement // بررسی وجود اسپینر
});
if (searchResultsContainer) { // نمایش خطا روی صفحه
searchResultsContainer.innerHTML = `<p style=\"color: red;\">${errorMessage}</p>`;
}
// نمایش خطا در المان های وضعیت و خطای جداگانه
updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true, 'error');
updateSelectionError(errorMessage);
// غیرفعال نگه داشتن دکمه جستجو
if (searchButton) {
setButtonEnabled(false);
}
// نیازی به return نیست.
}
});
// پایان بلوک DOMContentLoaded و پایان کامل فایل script.js